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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4880a32558b5c8a6d08e18d32893e3ab8ae4f9e00ba97009b7c95525998a28a2
4
- data.tar.gz: 1244df8f3307dd271471f5a67d2b4b5439086ae78b63f44e1a0e1ecaecf12a40
3
+ metadata.gz: '092429bef8965edaeff3896552612b8007c00bc923fa3261961d2408e6f2f3d2'
4
+ data.tar.gz: 0220ec6d7b2549cff95cb52d46cb04cbe298cc0f8009c352edf32e4368424bf3
5
5
  SHA512:
6
- metadata.gz: c58a40c28a7adecf324099f056a06ca65710f4ba82f9b035fed5a886be061da9cf70259b8372063cdaf60505febe8253455e874b502312811652dedd013b042c
7
- data.tar.gz: 2c2c8887ef9da0f01ec7afc51c432f06fe9684e7b732659b3ae3994976d450fcffbaf0bcdb69344045f578126ee4127ebff891ec0b02d6a17ed9f29e3812a18c
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.1] - 2026-02-24
3
+ ## [0.5.7] - 2026-03-27
4
4
 
5
- ### Changed
6
- - **Engine capability refactor** — replaced three redundant capability flags (`supports_remote_ref?`, `supports_bidirectional?`, `supports_state?`) with a single `execution_engine?` method
7
- - Clearer semantics: execution engines (Ruby/JS/Python) vs reasoning engines (LLM)
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
- ## [0.5.0] - 2026-02-22
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
- - **Polyglot engine architecture** — run Ruby, JavaScript, and Python code side by side
14
- - JavaScript engine via `mini_racer` with automatic variable bridging
15
- - Python engine via `pycall` with automatic variable bridging
16
- - Bidirectional Python Ruby calling through pycall bridge
17
- - Engine interface abstraction (`Mana::Engines::Base`)
18
- - Language auto-detection for polyglot dispatch
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
- ### Changed
21
- - Refactored LLM logic into `Engines::LLM`, extracted from monolithic core
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
- - Polyglot engine bug fixes (CodeRabbit review feedback)
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
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2026 carlnoah6
3
+ Copyright (c) 2026 Carl Li
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
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-20250514"
218
+ c.model = "claude-sonnet-4-6"
137
219
  c.temperature = 0
138
- c.api_key = ENV["ANTHROPIC_API_KEY"]
139
- c.max_iterations = 50
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 = 200_000 # nil = auto-detect from model
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-20250514"
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
- ### Custom effect handlers
270
+ ### Security policy
187
271
 
188
- Define your own tools that the LLM can call. Each effect becomes an LLM tool automatically — the block's keyword parameters define the tool's input schema.
272
+ Mana restricts what the LLM can call via security levels (higher = more permissions):
189
273
 
190
- ```ruby
191
- # No params
192
- Mana.define_effect :get_time do
193
- Time.now.to_s
194
- end
195
-
196
- # With params keyword args become tool parameters
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
- # With description (optional, recommended)
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
- # Use in prompts
208
- ~"get the current time and store in <now>"
209
- ~"find recent orders using query_db, store in <orders>"
284
+ ```ruby
285
+ Mana.configure { |c| c.security = :standard }
286
+ # or
287
+ Mana.configure { |c| c.security = 2 }
210
288
  ```
211
289
 
212
- Built-in effects (`read_var`, `write_var`, `read_attr`, `write_attr`, `call_func`, `done`) are reserved and cannot be overridden.
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
- ~"remember: always translate to Japanese, casual tone"
220
- ~"translate <text1>, store in <result1>" # uses the preference
221
- ~"translate <text2>, store in <result2>" # still remembers
222
- ~"which translation was harder? store in <analysis>" # can reference both
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
- Memory is per-thread and auto-created on the first `~"..."` call.
301
+ ### Function discovery
226
302
 
227
- #### Long-term memory
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
- ~"remember that the user prefers concise output"
233
- # ... later, in a different script execution ...
234
- ~"translate <text>" # LLM sees the preference in its long-term memory
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
- Manage long-term memory via Ruby:
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
- ```ruby
240
- Mana.memory.long_term # view all memories
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
- #### Incognito mode
248
-
249
- Run without any memory — nothing is loaded or saved:
250
-
251
- ```ruby
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
- ### Polyglot Cross-Language Interop
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
- `~"..."` is a universal operator. It detects whether the code is JavaScript, Python, Ruby, or natural language, and routes to the appropriate engine. Variables bridge automatically.
332
+ ### Memory
260
333
 
261
- #### JavaScript
334
+ Mana has two types of memory:
262
335
 
263
- ```ruby
264
- require "mana"
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
- data = [1, 2, 3, 4, 5]
339
+ #### Short-term memory (conversation context)
267
340
 
268
- # JavaScript auto-detected from syntax
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
- # Multi-line with heredoc
273
- ~<<~JS
274
- const sum = evens.reduce((a, b) => a + b, 0)
275
- const avg = sum / evens.length
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
- #### Python
349
+ Short-term memory is per-thread and auto-created on the first `~"..."` call.
281
350
 
282
351
  ```ruby
283
- # Python auto-detected
284
- ~"evens = [n for n in data if n % 2 == 0]"
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
- #### Natural language (LLM) — existing behavior
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
- ~"analyze <data> and find outliers, store in <result>"
300
- puts result
301
- ```
361
+ # script_1.rb
362
+ ~"remember that the user prefers concise output"
302
363
 
303
- #### How detection works
364
+ # script_2.rb (later, separate execution)
365
+ ~"translate <text>" # LLM sees "user prefers concise output" in long-term memory
366
+ ```
304
367
 
305
- - Auto-detects from code syntax (token patterns)
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
- #### Variable bridging
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
- - Simple types (numbers, strings, booleans, nil, arrays, hashes) are copied
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
- #### Setup
379
+ Run without any memory — nothing is loaded or saved:
317
380
 
318
381
  ```ruby
319
- # Gemfile
320
- gem "mana"
321
- gem "mini_racer" # for JavaScript support (optional)
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/test"
393
+ require "mana"
331
394
 
332
395
  RSpec.describe MyApp do
333
396
  include Mana::TestHelpers
334
397
 
335
- it "analyzes code" do
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
- result = MyApp.analyze("user_input")
339
- expect(result[:bugs]).to include("XSS")
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 "translates with dynamic response" do
343
- mock_prompt(/translate.*to\s+\w+/) do |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
- expect(MyApp.translate("hi", "Chinese")).to eq("你好")
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.base_url}/v1/messages")
12
- body = {
13
- model: model,
14
- max_tokens: max_tokens,
15
- system: system,
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