ruby-mana 0.5.1 → 0.5.7
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 +70 -17
- data/LICENSE +1 -1
- data/README.md +189 -166
- data/lib/mana/backends/anthropic.rb +9 -27
- data/lib/mana/backends/base.rb +51 -4
- data/lib/mana/backends/openai.rb +17 -42
- data/lib/mana/compiler.rb +162 -46
- data/lib/mana/config.rb +94 -6
- data/lib/mana/engine.rb +628 -38
- data/lib/mana/introspect.rb +58 -19
- data/lib/mana/logger.rb +99 -0
- data/lib/mana/memory.rb +132 -39
- data/lib/mana/memory_store.rb +18 -8
- data/lib/mana/mixin.rb +2 -2
- data/lib/mana/mock.rb +40 -0
- data/lib/mana/security_policy.rb +195 -0
- data/lib/mana/version.rb +1 -1
- data/lib/mana.rb +7 -30
- metadata +12 -38
- data/data/lang-rules.yml +0 -196
- data/lib/mana/backends/registry.rb +0 -23
- data/lib/mana/context_window.rb +0 -28
- data/lib/mana/effect_registry.rb +0 -155
- data/lib/mana/engines/base.rb +0 -79
- data/lib/mana/engines/detect.rb +0 -93
- data/lib/mana/engines/javascript.rb +0 -314
- data/lib/mana/engines/llm.rb +0 -467
- data/lib/mana/engines/python.rb +0 -314
- data/lib/mana/engines/ruby_eval.rb +0 -11
- data/lib/mana/namespace.rb +0 -39
- data/lib/mana/object_registry.rb +0 -89
- data/lib/mana/remote_ref.rb +0 -85
- data/lib/mana/test.rb +0 -18
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: '092429bef8965edaeff3896552612b8007c00bc923fa3261961d2408e6f2f3d2'
|
|
4
|
+
data.tar.gz: 0220ec6d7b2549cff95cb52d46cb04cbe298cc0f8009c352edf32e4368424bf3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a7d698cf965ed5fa907b1fbed85f09077b0324d9213e507c924ae2913fe9a2acf2897eda0508eb612e5e76a0a47cc3ba700ce4be0f89b1a35727002d503a8635
|
|
7
|
+
data.tar.gz: 365dfc57ee72353d49a737a468010e3117c760f417d2bb72fe9957d7609e73441d652acca6010aa5675857c2dce85fa8619b755151090cd19a1a59dbe874781b
|
data/CHANGELOG.md
CHANGED
|
@@ -1,27 +1,82 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## [0.5.
|
|
3
|
+
## [0.5.7] - 2026-03-27
|
|
4
4
|
|
|
5
|
-
###
|
|
6
|
-
- **
|
|
7
|
-
-
|
|
8
|
-
- Fully backward compatible — old methods still work as derived properties
|
|
5
|
+
### Security
|
|
6
|
+
- **call_func receiver validation** — blocks expression injection (e.g. `ENV['HOME'].to_s`) by requiring simple constant names only
|
|
7
|
+
- **write_var no longer pollutes receiver** — singleton method fallback only for new variables that don't conflict with existing methods
|
|
9
8
|
|
|
10
|
-
|
|
9
|
+
### Fixed
|
|
10
|
+
- Failed LLM calls no longer pollute short-term memory (messages rolled back on exception)
|
|
11
|
+
- Logger extracted from engine.rb (726→639 lines)
|
|
12
|
+
- Compiler cache includes sibling function signatures (dependency changes invalidate cache)
|
|
13
|
+
- Summarize error handling: ConfigError propagates, others log
|
|
14
|
+
- OpenAI convert_tools filters $schema key
|
|
15
|
+
- docs/index.html version badge, mock example, context_window default
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
- `Backends::Base` class with shared HTTP infrastructure
|
|
19
|
+
- `Logger` module extracted from Engine
|
|
20
|
+
- Tests for read_attr/write_attr error paths, nested error recovery, mixin visibility
|
|
21
|
+
- Examples: yard_comments.rb, testing.rb
|
|
22
|
+
|
|
23
|
+
## [0.5.6] - 2026-03-26
|
|
11
24
|
|
|
12
25
|
### Added
|
|
13
|
-
- **
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
-
|
|
18
|
-
-
|
|
26
|
+
- **Function comment extraction** — descriptions and @param types from YARD-style comments
|
|
27
|
+
- **Keyword argument support** in call_func (kwargs field)
|
|
28
|
+
- **Prism AST prompt extraction** — replaces regex, handles multi-line and escaped quotes
|
|
29
|
+
- **Instruction sequence fallback** — extracts prompt from bytecode in IRB/eval
|
|
30
|
+
- **Cache version locking** — includes gem version + Ruby version in cache hash
|
|
31
|
+
- **Method visibility preservation** — mana def respects private/protected
|
|
32
|
+
- **Smart log formatting** — code highlighting, auto-summarize long values
|
|
33
|
+
|
|
34
|
+
### Removed
|
|
35
|
+
- **effect_registry** — replaced by plain Ruby functions with comment extraction
|
|
36
|
+
|
|
37
|
+
### Fixed
|
|
38
|
+
- Config naming: unified `security` / `security=` (was `security_policy`)
|
|
39
|
+
- Config validation: `validate!` method, timeout=0 blocked
|
|
40
|
+
- Config decoupled from Backends module
|
|
41
|
+
- Compiler: `:nokey` (**nil) parameter handled
|
|
42
|
+
- Compiler: cache works in IRB with auto-invalidation
|
|
43
|
+
|
|
44
|
+
## [0.5.5] - 2026-03-23
|
|
19
45
|
|
|
20
|
-
###
|
|
21
|
-
-
|
|
46
|
+
### Added
|
|
47
|
+
- **Configurable security policy** — 5 levels from `:sandbox` (0) to `:danger` (4), with fine-grained `allow_receiver`/`block_method` overrides
|
|
48
|
+
- **Environment variable config** — `MANA_MODEL`, `MANA_VERBOSE`, `MANA_TIMEOUT`, `MANA_BACKEND`, `MANA_SECURITY`
|
|
49
|
+
- **Verbose mode** — `c.verbose = true` logs LLM calls, tool usage, and results to stderr
|
|
50
|
+
- **API key validation** — clear `ConfigError` with setup instructions instead of cryptic HTTP 401
|
|
51
|
+
- **Long-term memory deduplication** — identical content is no longer stored twice
|
|
52
|
+
- **Current prompt overrides memory** — explicit priority rule in system prompt
|
|
53
|
+
- **Ruby 4.0 support** — CI tests Ruby 3.3, 3.4, and 4.0
|
|
22
54
|
|
|
23
55
|
### Fixed
|
|
24
|
-
-
|
|
56
|
+
- `write_var` works on Ruby 4.0 without pre-declaring variables (singleton method fallback)
|
|
57
|
+
- Blocked Ruby introspection methods (`methods`, `local_variables`, etc.) in `call_func`
|
|
58
|
+
- LLM retries once when model skips tool calling and returns text only
|
|
59
|
+
- Long-term memory stored in `~/.mana/` instead of platform-specific paths
|
|
60
|
+
- Default model changed to `claude-sonnet-4-6`
|
|
61
|
+
- Compiler uses isolated binding to prevent `generate()` recursion
|
|
62
|
+
- Cache files named by source path with prompt hash validation
|
|
63
|
+
|
|
64
|
+
### Removed
|
|
65
|
+
- Polyglot engine system (JavaScript, Python, language detection)
|
|
66
|
+
- `mini_racer` and `pycall` dependencies
|
|
67
|
+
- `ObjectRegistry`, `RemoteRef`, engine capability queries
|
|
68
|
+
|
|
69
|
+
## [0.5.3] - 2026-03-22
|
|
70
|
+
|
|
71
|
+
### Added
|
|
72
|
+
- **Timeout configuration** — `timeout` option in `Mana.configure` (default: 120 seconds)
|
|
73
|
+
|
|
74
|
+
## [0.5.2] - 2026-03-22
|
|
75
|
+
|
|
76
|
+
### Added
|
|
77
|
+
- **API URL endpoint configuration** — `effective_base_url` auto-resolves per backend
|
|
78
|
+
- `ANTHROPIC_API_URL` / `OPENAI_API_URL` environment variables
|
|
79
|
+
- API key fallback: `ANTHROPIC_API_KEY` → `OPENAI_API_KEY`
|
|
25
80
|
|
|
26
81
|
## [0.4.0] - 2026-02-22
|
|
27
82
|
|
|
@@ -32,14 +87,12 @@
|
|
|
32
87
|
|
|
33
88
|
- Nested prompts — LLM calling LLM
|
|
34
89
|
- Lambda `call_func` support
|
|
35
|
-
- Expanded test coverage
|
|
36
90
|
|
|
37
91
|
## [0.3.0] - 2026-02-21
|
|
38
92
|
|
|
39
93
|
- Automatic memory — context sharing across LLM calls
|
|
40
94
|
- Incognito mode
|
|
41
95
|
- Persistent long-term memory
|
|
42
|
-
- `Mana.session` — shared conversation context across prompts
|
|
43
96
|
|
|
44
97
|
## [0.2.0] - 2026-02-20
|
|
45
98
|
|
data/LICENSE
CHANGED
data/README.md
CHANGED
|
@@ -28,14 +28,18 @@ Or in your Gemfile:
|
|
|
28
28
|
gem "ruby-mana"
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
-
Requires Ruby 3.3+ and an API key (Anthropic, OpenAI, or compatible):
|
|
31
|
+
Requires Ruby 3.3+ (including 4.0) and an API key (Anthropic, OpenAI, or compatible):
|
|
32
32
|
|
|
33
33
|
```bash
|
|
34
34
|
export ANTHROPIC_API_KEY=your_key_here
|
|
35
|
+
export ANTHROPIC_API_URL=https://api.anthropic.com # optional, this is the default
|
|
35
36
|
# or
|
|
36
37
|
export OPENAI_API_KEY=your_key_here
|
|
38
|
+
export OPENAI_API_URL=https://api.openai.com # optional, this is the default
|
|
37
39
|
```
|
|
38
40
|
|
|
41
|
+
Supports Ruby 3.3, 3.4, and 4.0 — no API differences between versions.
|
|
42
|
+
|
|
39
43
|
## Usage
|
|
40
44
|
|
|
41
45
|
Prefix any string with `~` to make it an LLM prompt:
|
|
@@ -129,18 +133,98 @@ while player_hp > 0 && enemy_hp > 0
|
|
|
129
133
|
end
|
|
130
134
|
```
|
|
131
135
|
|
|
136
|
+
### Nested prompts
|
|
137
|
+
|
|
138
|
+
Functions called by LLM can themselves contain `~"..."` prompts:
|
|
139
|
+
|
|
140
|
+
```ruby
|
|
141
|
+
lint = ->(code) { ~"check #{code} for style issues, store in <issues>" }
|
|
142
|
+
# Equivalent to:
|
|
143
|
+
# def lint(code)
|
|
144
|
+
# ~"check #{code} for style issues, store in <issues>"
|
|
145
|
+
# issues
|
|
146
|
+
# end
|
|
147
|
+
|
|
148
|
+
~"review <codebase>, call lint for each file, store report in <report>"
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Each nested call gets its own conversation context. The outer LLM only sees the function's return value, keeping its context clean.
|
|
152
|
+
|
|
153
|
+
### LLM-compiled methods
|
|
154
|
+
|
|
155
|
+
`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.
|
|
156
|
+
|
|
157
|
+
```ruby
|
|
158
|
+
mana def fibonacci(n)
|
|
159
|
+
~"return an array of the first n Fibonacci numbers"
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
fibonacci(10) # first call → LLM generates code → cached → executed
|
|
163
|
+
fibonacci(20) # pure Ruby from .mana_cache/
|
|
164
|
+
|
|
165
|
+
# View the generated source
|
|
166
|
+
puts Mana.source(:fibonacci)
|
|
167
|
+
# def fibonacci(n)
|
|
168
|
+
# return [] if n <= 0
|
|
169
|
+
# return [0] if n == 1
|
|
170
|
+
# fib = [0, 1]
|
|
171
|
+
# (2...n).each { |i| fib << fib[i-1] + fib[i-2] }
|
|
172
|
+
# fib
|
|
173
|
+
# end
|
|
174
|
+
|
|
175
|
+
# Works in classes too
|
|
176
|
+
class Converter
|
|
177
|
+
include Mana::Mixin
|
|
178
|
+
|
|
179
|
+
mana def celsius_to_fahrenheit(c)
|
|
180
|
+
~"convert Celsius to Fahrenheit"
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Generated files live in `.mana_cache/` (add to `.gitignore`, or commit them to skip LLM on CI).
|
|
186
|
+
|
|
132
187
|
## Configuration
|
|
133
188
|
|
|
189
|
+
All options can be set via environment variables (`.env` file) or `Mana.configure`:
|
|
190
|
+
|
|
191
|
+
```bash
|
|
192
|
+
# .env — just source it: `source .env`
|
|
193
|
+
export ANTHROPIC_API_KEY=sk-your-key-here
|
|
194
|
+
export ANTHROPIC_API_URL=https://api.anthropic.com # optional, custom endpoint
|
|
195
|
+
export MANA_MODEL=claude-sonnet-4-6 # default model
|
|
196
|
+
export MANA_VERBOSE=true # show LLM interactions
|
|
197
|
+
export MANA_TIMEOUT=120 # HTTP timeout in seconds
|
|
198
|
+
export MANA_BACKEND=anthropic # force backend (anthropic/openai)
|
|
199
|
+
export MANA_SECURITY=standard # security level (0-4 or name)
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
| Environment Variable | Config | Default | Description |
|
|
203
|
+
|---------------------|--------|---------|-------------|
|
|
204
|
+
| `ANTHROPIC_API_KEY` | `c.api_key` | — | API key (required) |
|
|
205
|
+
| `OPENAI_API_KEY` | `c.api_key` | — | Fallback API key |
|
|
206
|
+
| `ANTHROPIC_API_URL` | `c.base_url` | auto-detect | Custom API endpoint |
|
|
207
|
+
| `OPENAI_API_URL` | `c.base_url` | auto-detect | Fallback endpoint |
|
|
208
|
+
| `MANA_MODEL` | `c.model` | `claude-sonnet-4-6` | LLM model name |
|
|
209
|
+
| `MANA_VERBOSE` | `c.verbose` | `false` | Log LLM calls to stderr |
|
|
210
|
+
| `MANA_TIMEOUT` | `c.timeout` | `120` | HTTP timeout (seconds) |
|
|
211
|
+
| `MANA_BACKEND` | `c.backend` | auto-detect | Force `anthropic` or `openai` |
|
|
212
|
+
| `MANA_SECURITY` | `c.security` | `:standard` (2) | Security level: `sandbox`, `strict`, `standard`, `permissive`, `danger` |
|
|
213
|
+
|
|
214
|
+
Programmatic config (overrides env vars):
|
|
215
|
+
|
|
134
216
|
```ruby
|
|
135
217
|
Mana.configure do |c|
|
|
136
|
-
c.model = "claude-sonnet-4-
|
|
218
|
+
c.model = "claude-sonnet-4-6"
|
|
137
219
|
c.temperature = 0
|
|
138
|
-
c.api_key =
|
|
139
|
-
c.
|
|
220
|
+
c.api_key = "sk-..."
|
|
221
|
+
c.verbose = true
|
|
222
|
+
c.timeout = 120
|
|
223
|
+
c.security = :strict # security level (0-4 or symbol)
|
|
140
224
|
|
|
141
225
|
# Memory settings
|
|
142
226
|
c.namespace = "my-project" # nil = auto-detect from git/pwd
|
|
143
|
-
c.context_window =
|
|
227
|
+
c.context_window = 128_000 # default: 128_000
|
|
144
228
|
c.memory_pressure = 0.7 # compact when tokens exceed 70% of context window
|
|
145
229
|
c.memory_keep_recent = 4 # keep last 4 rounds during compaction
|
|
146
230
|
c.compact_model = nil # nil = use main model for compaction
|
|
@@ -156,7 +240,7 @@ Mana supports Anthropic and OpenAI-compatible APIs (including Ollama, DeepSeek,
|
|
|
156
240
|
# Anthropic (default for claude-* models)
|
|
157
241
|
Mana.configure do |c|
|
|
158
242
|
c.api_key = ENV["ANTHROPIC_API_KEY"]
|
|
159
|
-
c.model = "claude-sonnet-4-
|
|
243
|
+
c.model = "claude-sonnet-4-6"
|
|
160
244
|
end
|
|
161
245
|
|
|
162
246
|
# OpenAI
|
|
@@ -183,172 +267,167 @@ end
|
|
|
183
267
|
|
|
184
268
|
Backend is auto-detected from model name: `claude-*` → Anthropic, everything else → OpenAI.
|
|
185
269
|
|
|
186
|
-
###
|
|
270
|
+
### Security policy
|
|
187
271
|
|
|
188
|
-
|
|
272
|
+
Mana restricts what the LLM can call via security levels (higher = more permissions):
|
|
189
273
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
Mana.define_effect :query_db do |sql:|
|
|
198
|
-
ActiveRecord::Base.connection.execute(sql).to_a
|
|
199
|
-
end
|
|
274
|
+
| Level | Name | What LLM Can Do | What's Blocked |
|
|
275
|
+
|-------|------|-----------------|----------------|
|
|
276
|
+
| 0 | `:sandbox` | Read/write variables, call user-defined functions only | Everything else |
|
|
277
|
+
| 1 | `:strict` | + safe stdlib (`Time.now`, `Date.today`, `Math.*`) | Filesystem, network, system calls, eval |
|
|
278
|
+
| **2** | **`:standard`** (default) | + read filesystem (`File.read`, `Dir.glob`) | Write/delete files, network, eval |
|
|
279
|
+
| 3 | `:permissive` | + write files, network, require | eval, system/exec/fork |
|
|
280
|
+
| 4 | `:danger` | No restrictions | Nothing |
|
|
200
281
|
|
|
201
|
-
|
|
202
|
-
Mana.define_effect :search_web,
|
|
203
|
-
description: "Search the web for information" do |query:, max_results: 5|
|
|
204
|
-
WebSearch.search(query, limit: max_results)
|
|
205
|
-
end
|
|
282
|
+
Default is **level 2 (`:standard`)**. Set via config or env var:
|
|
206
283
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
284
|
+
```ruby
|
|
285
|
+
Mana.configure { |c| c.security = :standard }
|
|
286
|
+
# or
|
|
287
|
+
Mana.configure { |c| c.security = 2 }
|
|
210
288
|
```
|
|
211
289
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
### Memory — automatic context sharing
|
|
215
|
-
|
|
216
|
-
Consecutive `~"..."` calls automatically share context. No wrapper block needed:
|
|
290
|
+
Fine-grained overrides:
|
|
217
291
|
|
|
218
292
|
```ruby
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
293
|
+
Mana.configure do |c|
|
|
294
|
+
c.security = :strict
|
|
295
|
+
c.security.allow_receiver "File", only: %w[read exist?]
|
|
296
|
+
c.security.block_method "puts"
|
|
297
|
+
c.security.block_receiver "Net::HTTP"
|
|
298
|
+
end
|
|
223
299
|
```
|
|
224
300
|
|
|
225
|
-
|
|
301
|
+
### Function discovery
|
|
226
302
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
The LLM has a `remember` tool that persists facts across script executions:
|
|
303
|
+
Mana automatically discovers your Ruby functions and makes them available to the LLM. Add comments above your functions for better LLM understanding:
|
|
230
304
|
|
|
231
305
|
```ruby
|
|
232
|
-
|
|
233
|
-
#
|
|
234
|
-
|
|
235
|
-
|
|
306
|
+
# Query the database and return results
|
|
307
|
+
# @param sql [String] the SQL query
|
|
308
|
+
# @param limit [Integer] maximum rows to return
|
|
309
|
+
def query_db(sql:, limit: 10)
|
|
310
|
+
ActiveRecord::Base.connection.execute(sql).first(limit)
|
|
311
|
+
end
|
|
236
312
|
|
|
237
|
-
|
|
313
|
+
# Search the web for information
|
|
314
|
+
# @param query [String] search keywords
|
|
315
|
+
def search_web(query:)
|
|
316
|
+
WebSearch.search(query)
|
|
317
|
+
end
|
|
238
318
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
Mana.memory.forget(id: 2) # remove a specific memory
|
|
242
|
-
Mana.memory.clear_long_term! # clear all long-term memories
|
|
243
|
-
Mana.memory.clear_short_term! # clear conversation history
|
|
244
|
-
Mana.memory.clear! # clear everything
|
|
319
|
+
~"use query_db to find recent orders, store in <orders>"
|
|
320
|
+
~"search_web for 'ruby mana gem', store in <results>"
|
|
245
321
|
```
|
|
246
322
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
Mana.incognito do
|
|
253
|
-
~"translate <text>" # no memory, no persistence
|
|
254
|
-
end
|
|
323
|
+
The LLM sees:
|
|
324
|
+
```
|
|
325
|
+
Available Ruby functions:
|
|
326
|
+
query_db(sql:, limit: ...) — Query the database and return results
|
|
327
|
+
search_web(query:) — Search the web for information
|
|
255
328
|
```
|
|
256
329
|
|
|
257
|
-
|
|
330
|
+
Both positional and keyword arguments are supported. Functions are discovered from the source file (via Prism AST) and from methods defined on `self`.
|
|
258
331
|
|
|
259
|
-
|
|
332
|
+
### Memory
|
|
260
333
|
|
|
261
|
-
|
|
334
|
+
Mana has two types of memory:
|
|
262
335
|
|
|
263
|
-
|
|
264
|
-
|
|
336
|
+
- **Short-term memory** — conversation history within the current process. Each `~"..."` call appends to it, so consecutive calls share context. Cleared when the process exits.
|
|
337
|
+
- **Long-term memory** — persistent facts stored on disk. Survives across script executions. The LLM can save facts via the `remember` tool.
|
|
265
338
|
|
|
266
|
-
|
|
339
|
+
#### Short-term memory (conversation context)
|
|
267
340
|
|
|
268
|
-
|
|
269
|
-
~"const evens = data.filter(n => n % 2 === 0)"
|
|
270
|
-
puts evens # => [2, 4]
|
|
341
|
+
Consecutive `~"..."` calls automatically share context. No wrapper block needed:
|
|
271
342
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
JS
|
|
277
|
-
puts avg # => 3.0
|
|
343
|
+
```ruby
|
|
344
|
+
~"translate <text1> to Japanese, store in <result1>"
|
|
345
|
+
~"translate <text2> to the same language, store in <result2>" # remembers "Japanese"
|
|
346
|
+
~"which translation was harder? store in <analysis>" # can reference both
|
|
278
347
|
```
|
|
279
348
|
|
|
280
|
-
|
|
349
|
+
Short-term memory is per-thread and auto-created on the first `~"..."` call.
|
|
281
350
|
|
|
282
351
|
```ruby
|
|
283
|
-
#
|
|
284
|
-
|
|
285
|
-
puts evens # => [2, 4]
|
|
286
|
-
|
|
287
|
-
# Multi-line
|
|
288
|
-
~<<~PY
|
|
289
|
-
import statistics
|
|
290
|
-
mean = statistics.mean(data)
|
|
291
|
-
stdev = statistics.stdev(data)
|
|
292
|
-
PY
|
|
293
|
-
puts mean # => 3.0
|
|
352
|
+
Mana.memory.short_term # view conversation history
|
|
353
|
+
Mana.memory.clear_short_term! # clear conversation history
|
|
294
354
|
```
|
|
295
355
|
|
|
296
|
-
####
|
|
356
|
+
#### Long-term memory (persistent facts)
|
|
357
|
+
|
|
358
|
+
The LLM has a `remember` tool that persists facts to disk. These survive across script executions:
|
|
297
359
|
|
|
298
360
|
```ruby
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
```
|
|
361
|
+
# script_1.rb
|
|
362
|
+
~"remember that the user prefers concise output"
|
|
302
363
|
|
|
303
|
-
|
|
364
|
+
# script_2.rb (later, separate execution)
|
|
365
|
+
~"translate <text>" # LLM sees "user prefers concise output" in long-term memory
|
|
366
|
+
```
|
|
304
367
|
|
|
305
|
-
|
|
306
|
-
- Context-aware: consecutive `~"..."` calls tend to stay in the same language
|
|
307
|
-
- Override with `Mana.engine = :javascript` or `Mana.with(:python) { ... }`
|
|
308
|
-
- Detection rules are defined in `data/lang-rules.yml` — transparent, no black box
|
|
368
|
+
Identical content is automatically deduplicated.
|
|
309
369
|
|
|
310
|
-
|
|
370
|
+
```ruby
|
|
371
|
+
Mana.memory.long_term # view all persisted facts
|
|
372
|
+
Mana.memory.forget(id: 2) # remove a specific fact
|
|
373
|
+
Mana.memory.clear_long_term! # clear all long-term memory
|
|
374
|
+
Mana.memory.clear! # clear both short-term and long-term
|
|
375
|
+
```
|
|
311
376
|
|
|
312
|
-
|
|
313
|
-
- Each engine maintains a persistent context (V8 for JS, Python interpreter for Python)
|
|
314
|
-
- Variables created in one `~"..."` call persist for the next call in the same engine
|
|
377
|
+
#### Incognito mode
|
|
315
378
|
|
|
316
|
-
|
|
379
|
+
Run without any memory — nothing is loaded or saved:
|
|
317
380
|
|
|
318
381
|
```ruby
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
gem "pycall" # for Python support (optional)
|
|
382
|
+
Mana.incognito do
|
|
383
|
+
~"translate <text>" # no memory, no persistence
|
|
384
|
+
end
|
|
323
385
|
```
|
|
324
386
|
|
|
387
|
+
|
|
325
388
|
### Testing
|
|
326
389
|
|
|
327
390
|
Use `Mana.mock` to test code that uses `~"..."` without calling any API:
|
|
328
391
|
|
|
329
392
|
```ruby
|
|
330
|
-
require "mana
|
|
393
|
+
require "mana"
|
|
331
394
|
|
|
332
395
|
RSpec.describe MyApp do
|
|
333
396
|
include Mana::TestHelpers
|
|
334
397
|
|
|
335
|
-
it "
|
|
398
|
+
it "writes variables into caller scope" do
|
|
399
|
+
# Each key becomes a local variable via write_var
|
|
336
400
|
mock_prompt "analyze", bugs: ["XSS"], score: 8.5
|
|
337
401
|
|
|
338
|
-
|
|
339
|
-
expect(
|
|
402
|
+
~"analyze <code> and store bugs in <bugs> and score in <score>"
|
|
403
|
+
expect(bugs).to eq(["XSS"])
|
|
404
|
+
expect(score).to eq(8.5)
|
|
340
405
|
end
|
|
341
406
|
|
|
342
|
-
it "
|
|
343
|
-
mock_prompt
|
|
407
|
+
it "returns a value via _return" do
|
|
408
|
+
mock_prompt "translate", _return: "你好"
|
|
409
|
+
|
|
410
|
+
result = ~"translate hello to Chinese"
|
|
411
|
+
expect(result).to eq("你好")
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
it "uses block for dynamic responses" do
|
|
415
|
+
mock_prompt(/translate/) do |prompt|
|
|
344
416
|
{ output: prompt.include?("Chinese") ? "你好" : "hello" }
|
|
345
417
|
end
|
|
346
418
|
|
|
347
|
-
|
|
419
|
+
~"translate hi to Chinese, store in <output>"
|
|
420
|
+
expect(output).to eq("你好")
|
|
348
421
|
end
|
|
349
422
|
end
|
|
350
423
|
```
|
|
351
424
|
|
|
425
|
+
**How mock works:**
|
|
426
|
+
- `mock_prompt(pattern, key: value, ...)` — each key/value pair is written as a local variable (simulates `write_var`)
|
|
427
|
+
- `_return:` — special key, becomes the return value of `~"..."`
|
|
428
|
+
- Block form — receives the prompt text, returns a hash of variables to write
|
|
429
|
+
- Pattern matching: `String` uses `include?`, `Regexp` uses `match?`
|
|
430
|
+
|
|
352
431
|
Block mode for inline tests:
|
|
353
432
|
|
|
354
433
|
```ruby
|
|
@@ -363,59 +442,6 @@ end
|
|
|
363
442
|
|
|
364
443
|
Unmatched prompts raise `Mana::MockError` with a helpful message suggesting the stub to add.
|
|
365
444
|
|
|
366
|
-
### Nested prompts
|
|
367
|
-
|
|
368
|
-
Functions called by LLM can themselves contain `~"..."` prompts:
|
|
369
|
-
|
|
370
|
-
```ruby
|
|
371
|
-
lint = ->(code) { ~"check #{code} for style issues, store in <issues>" }
|
|
372
|
-
# Equivalent to:
|
|
373
|
-
# def lint(code)
|
|
374
|
-
# ~"check #{code} for style issues, store in <issues>"
|
|
375
|
-
# issues
|
|
376
|
-
# end
|
|
377
|
-
|
|
378
|
-
~"review <codebase>, call lint for each file, store report in <report>"
|
|
379
|
-
```
|
|
380
|
-
|
|
381
|
-
Each nested call gets its own conversation context. The outer LLM only sees the function's return value, keeping its context clean.
|
|
382
|
-
|
|
383
|
-
### LLM-compiled methods
|
|
384
|
-
|
|
385
|
-
`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.
|
|
386
|
-
|
|
387
|
-
```ruby
|
|
388
|
-
mana def fizzbuzz(n)
|
|
389
|
-
~"return an array of FizzBuzz results from 1 to n"
|
|
390
|
-
end
|
|
391
|
-
|
|
392
|
-
fizzbuzz(15) # first call → LLM generates code → cached → executed
|
|
393
|
-
fizzbuzz(20) # pure Ruby from .mana_cache/fizzbuzz.rb
|
|
394
|
-
|
|
395
|
-
# View the generated source
|
|
396
|
-
puts Mana.source(:fizzbuzz)
|
|
397
|
-
# def fizzbuzz(n)
|
|
398
|
-
# (1..n).map do |i|
|
|
399
|
-
# if i % 15 == 0 then "FizzBuzz"
|
|
400
|
-
# elsif i % 3 == 0 then "Fizz"
|
|
401
|
-
# elsif i % 5 == 0 then "Buzz"
|
|
402
|
-
# else i.to_s
|
|
403
|
-
# end
|
|
404
|
-
# end
|
|
405
|
-
# end
|
|
406
|
-
|
|
407
|
-
# Works in classes too
|
|
408
|
-
class Converter
|
|
409
|
-
include Mana::Mixin
|
|
410
|
-
|
|
411
|
-
mana def celsius_to_fahrenheit(c)
|
|
412
|
-
~"convert Celsius to Fahrenheit"
|
|
413
|
-
end
|
|
414
|
-
end
|
|
415
|
-
```
|
|
416
|
-
|
|
417
|
-
Generated files live in `.mana_cache/` (add to `.gitignore`, or commit them to skip LLM on CI).
|
|
418
|
-
|
|
419
445
|
## How it works
|
|
420
446
|
|
|
421
447
|
1. `~"..."` calls `String#~@`, which captures the caller's `Binding`
|
|
@@ -426,9 +452,6 @@ Generated files live in `.mana_cache/` (add to `.gitignore`, or commit them to s
|
|
|
426
452
|
6. Loop until LLM calls `done` or returns without tool calls
|
|
427
453
|
7. After completion, memory compaction runs in background if context is getting large
|
|
428
454
|
|
|
429
|
-
## Safety
|
|
430
|
-
|
|
431
|
-
⚠️ Mana executes LLM-generated operations against your live Ruby state. Use with the same caution as `eval`.
|
|
432
455
|
|
|
433
456
|
## License
|
|
434
457
|
|
|
@@ -1,36 +1,18 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "json"
|
|
4
|
-
require "net/http"
|
|
5
|
-
require "uri"
|
|
6
|
-
|
|
7
3
|
module Mana
|
|
8
4
|
module Backends
|
|
5
|
+
# Native Anthropic Claude backend.
|
|
6
|
+
#
|
|
7
|
+
# Sends requests directly to the Anthropic Messages API (/v1/messages).
|
|
8
|
+
# No format conversion needed — Mana's internal format matches Anthropic's.
|
|
9
9
|
class Anthropic < Base
|
|
10
10
|
def chat(system:, messages:, tools:, model:, max_tokens: 4096)
|
|
11
|
-
uri = URI("#{@config.
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
tools: tools,
|
|
17
|
-
messages: messages
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
http = Net::HTTP.new(uri.host, uri.port)
|
|
21
|
-
http.use_ssl = uri.scheme == "https"
|
|
22
|
-
http.read_timeout = 120
|
|
23
|
-
|
|
24
|
-
req = Net::HTTP::Post.new(uri)
|
|
25
|
-
req["Content-Type"] = "application/json"
|
|
26
|
-
req["x-api-key"] = @config.api_key
|
|
27
|
-
req["anthropic-version"] = "2023-06-01"
|
|
28
|
-
req.body = JSON.generate(body)
|
|
29
|
-
|
|
30
|
-
res = http.request(req)
|
|
31
|
-
raise LLMError, "HTTP #{res.code}: #{res.body}" unless res.is_a?(Net::HTTPSuccess)
|
|
32
|
-
|
|
33
|
-
parsed = JSON.parse(res.body, symbolize_names: true)
|
|
11
|
+
uri = URI("#{@config.effective_base_url}/v1/messages")
|
|
12
|
+
parsed = http_post(uri, { model:, max_tokens:, system:, tools:, messages: }, {
|
|
13
|
+
"x-api-key" => @config.api_key,
|
|
14
|
+
"anthropic-version" => "2023-06-01"
|
|
15
|
+
})
|
|
34
16
|
parsed[:content] || []
|
|
35
17
|
end
|
|
36
18
|
end
|