ruby-mana 0.5.12 → 0.5.13
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 +11 -0
- data/README.md +28 -3
- data/lib/mana/backends/anthropic.rb +13 -2
- data/lib/mana/backends/openai.rb +13 -1
- data/lib/mana/compiler.rb +1 -1
- data/lib/mana/engine.rb +30 -2
- data/lib/mana/knowledge.rb +1 -1
- data/lib/mana/logger.rb +0 -10
- data/lib/mana/memory_store.rb +4 -4
- data/lib/mana/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f01dc9677f8c7a7813e0d8df7ec44f0a8fe44c54342d1181f64edd9f34e452a2
|
|
4
|
+
data.tar.gz: 0da4c67c41015a2ce4bb6727a0aeeb2a0334a8f85da9a38f97e122cb294a3169
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 36207a5776806db60df0ce71e4f2d53ad1c48af6bdf43aeb7782035639038457a088fa508cc25afe197cbcaf0f02490503912bbabbda4229131f6e36f8befa9e
|
|
7
|
+
data.tar.gz: 4055b0f53f20a702e120c23801fd628ddc49643f5ad08b21aa8c02647661bd0516187af8e0a8887e90cf6bd5623efb31435ef5da3143d28f680125fcd6acb97a
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.5.13] - 2026-04-05
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
- Backends (`Anthropic`, `OpenAI`) now return `{ content: [...], usage: { input_tokens:, output_tokens: } }` instead of a plain content array
|
|
7
|
+
- Anthropic streaming (`chat_stream`) captures usage from `message_start` and `message_delta` events
|
|
8
|
+
- OpenAI backend normalizes `prompt_tokens`/`completion_tokens` to `input_tokens`/`output_tokens`
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- `Engine#trace_data` — after execution, exposes `{ prompt:, model:, steps:, total_iterations:, timestamp: }` with per-step usage, latency, and tool call details
|
|
12
|
+
- LLM call latency measurement via `Process.clock_gettime` in `Engine#llm_call`
|
|
13
|
+
|
|
3
14
|
## [0.5.12] - 2026-04-04
|
|
4
15
|
|
|
5
16
|
### Changed
|
data/README.md
CHANGED
|
@@ -144,7 +144,7 @@ end
|
|
|
144
144
|
puts Mana.source(:celsius_to_fahrenheit, owner: Converter)
|
|
145
145
|
```
|
|
146
146
|
|
|
147
|
-
Generated files live in `.
|
|
147
|
+
Generated files live in `.ruby-mana/cache/` (add to `.gitignore`, or commit them to skip LLM on CI).
|
|
148
148
|
|
|
149
149
|
## Advanced
|
|
150
150
|
|
|
@@ -228,7 +228,7 @@ Mana.configure do |c|
|
|
|
228
228
|
c.namespace = "my-project" # nil = auto-detect from git/pwd
|
|
229
229
|
c.context_window = 128_000 # default: 128_000
|
|
230
230
|
c.memory_store = Mana::FileStore.new # default file-based persistence
|
|
231
|
-
c.memory_path = ".mana"
|
|
231
|
+
c.memory_path = ".ruby-mana" # directory for memory files
|
|
232
232
|
c.context_class = nil # custom context class (e.g. from agent frameworks)
|
|
233
233
|
c.knowledge_provider = nil # custom knowledge provider
|
|
234
234
|
end
|
|
@@ -291,6 +291,31 @@ end
|
|
|
291
291
|
|
|
292
292
|
Unmatched prompts raise `Mana::MockError` with a helpful message suggesting the stub to add.
|
|
293
293
|
|
|
294
|
+
### Execution tracing
|
|
295
|
+
|
|
296
|
+
After each `execute` call, the engine exposes timing and token usage data:
|
|
297
|
+
|
|
298
|
+
```ruby
|
|
299
|
+
engine = Mana::Engine.new(binding)
|
|
300
|
+
result = engine.execute("compute <x>")
|
|
301
|
+
|
|
302
|
+
trace = engine.trace_data
|
|
303
|
+
# => {
|
|
304
|
+
# prompt: "compute <x>",
|
|
305
|
+
# model: "claude-sonnet-4-20250514",
|
|
306
|
+
# timestamp: "2026-04-05T10:30:00+08:00",
|
|
307
|
+
# total_iterations: 2,
|
|
308
|
+
# steps: [
|
|
309
|
+
# { iteration: 1, latency_ms: 800,
|
|
310
|
+
# usage: { input_tokens: 500, output_tokens: 200 },
|
|
311
|
+
# tool_calls: [{ name: "read_var", input: {...}, result: "..." }] },
|
|
312
|
+
# ...
|
|
313
|
+
# ]
|
|
314
|
+
# }
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
Backends return usage alongside content: `backend.chat(...)` returns `{ content: [...], usage: { input_tokens:, output_tokens: } }`.
|
|
318
|
+
|
|
294
319
|
## How it works
|
|
295
320
|
|
|
296
321
|
```
|
|
@@ -320,7 +345,7 @@ Unmatched prompts raise `Mana::MockError` with a helpful message suggesting the
|
|
|
320
345
|
|
|
321
346
|
3. **Build system prompt** — assembles rules, variable values, and function signatures into a single system prompt.
|
|
322
347
|
|
|
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`, `
|
|
348
|
+
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`, `knowledge`). 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.
|
|
324
349
|
|
|
325
350
|
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.
|
|
326
351
|
|
|
@@ -15,7 +15,10 @@ module Mana
|
|
|
15
15
|
"x-api-key" => @config.api_key,
|
|
16
16
|
"anthropic-version" => "2023-06-01"
|
|
17
17
|
})
|
|
18
|
-
|
|
18
|
+
{
|
|
19
|
+
content: parsed[:content] || [],
|
|
20
|
+
usage: parsed[:usage]
|
|
21
|
+
}
|
|
19
22
|
end
|
|
20
23
|
|
|
21
24
|
# Streaming variant — yields {type: :text_delta, text: "..."} events.
|
|
@@ -24,6 +27,7 @@ module Mana
|
|
|
24
27
|
uri = URI("#{@config.effective_base_url}/v1/messages")
|
|
25
28
|
content_blocks = []
|
|
26
29
|
current_block = nil
|
|
30
|
+
usage = {}
|
|
27
31
|
|
|
28
32
|
# Anthropic streams SSE events that incrementally build content blocks.
|
|
29
33
|
# We reassemble them into the same format that chat() returns.
|
|
@@ -34,6 +38,10 @@ module Mana
|
|
|
34
38
|
"anthropic-version" => "2023-06-01"
|
|
35
39
|
}) do |event|
|
|
36
40
|
case event[:type]
|
|
41
|
+
when "message_start"
|
|
42
|
+
usage[:input_tokens] = event.dig(:message, :usage, :input_tokens)
|
|
43
|
+
when "message_delta"
|
|
44
|
+
usage[:output_tokens] = event.dig(:usage, :output_tokens)
|
|
37
45
|
when "content_block_start"
|
|
38
46
|
current_block = event[:content_block].dup
|
|
39
47
|
# Tool input arrives as JSON fragments — accumulate as a string, parse on stop
|
|
@@ -60,7 +68,10 @@ module Mana
|
|
|
60
68
|
end
|
|
61
69
|
end
|
|
62
70
|
|
|
63
|
-
|
|
71
|
+
{
|
|
72
|
+
content: content_blocks,
|
|
73
|
+
usage: usage.empty? ? nil : usage
|
|
74
|
+
}
|
|
64
75
|
end
|
|
65
76
|
end
|
|
66
77
|
end
|
data/lib/mana/backends/openai.rb
CHANGED
|
@@ -21,7 +21,10 @@ module Mana
|
|
|
21
21
|
}, {
|
|
22
22
|
"Authorization" => "Bearer #{@config.api_key}"
|
|
23
23
|
})
|
|
24
|
-
|
|
24
|
+
{
|
|
25
|
+
content: normalize_response(parsed),
|
|
26
|
+
usage: normalize_usage(parsed[:usage])
|
|
27
|
+
}
|
|
25
28
|
end
|
|
26
29
|
|
|
27
30
|
private
|
|
@@ -124,6 +127,15 @@ module Mana
|
|
|
124
127
|
end
|
|
125
128
|
end
|
|
126
129
|
|
|
130
|
+
# Convert OpenAI usage fields to our standard format.
|
|
131
|
+
def normalize_usage(usage)
|
|
132
|
+
return nil unless usage
|
|
133
|
+
{
|
|
134
|
+
input_tokens: usage[:prompt_tokens],
|
|
135
|
+
output_tokens: usage[:completion_tokens]
|
|
136
|
+
}
|
|
137
|
+
end
|
|
138
|
+
|
|
127
139
|
# Convert OpenAI response to Anthropic-style content blocks.
|
|
128
140
|
# This normalization lets the rest of the engine work with a single format
|
|
129
141
|
# regardless of which backend was used.
|
data/lib/mana/compiler.rb
CHANGED
data/lib/mana/engine.rb
CHANGED
|
@@ -6,7 +6,7 @@ module Mana
|
|
|
6
6
|
# The Engine handles ~"..." prompts by calling an LLM with tool-calling
|
|
7
7
|
# to interact with Ruby variables in the caller's binding.
|
|
8
8
|
class Engine
|
|
9
|
-
attr_reader :config, :binding
|
|
9
|
+
attr_reader :config, :binding, :trace_data
|
|
10
10
|
|
|
11
11
|
include Mana::BindingHelpers
|
|
12
12
|
include Mana::PromptBuilder
|
|
@@ -187,6 +187,7 @@ module Mana
|
|
|
187
187
|
iterations = 0
|
|
188
188
|
done_result = nil
|
|
189
189
|
@written_vars = {} # Track write_var calls for return value
|
|
190
|
+
@_steps = [] # Trace data: per-iteration usage + timing + tool calls
|
|
190
191
|
|
|
191
192
|
vlog("═" * 60)
|
|
192
193
|
vlog("🚀 Prompt: #{prompt}")
|
|
@@ -241,6 +242,13 @@ module Mana
|
|
|
241
242
|
{ type: "tool_result", tool_use_id: tu[:id], content: result.to_s }
|
|
242
243
|
end
|
|
243
244
|
|
|
245
|
+
# Record tool calls in trace step
|
|
246
|
+
if @_steps.last
|
|
247
|
+
@_steps.last[:tool_calls] = tool_uses.zip(tool_results).map { |tu, tr|
|
|
248
|
+
{ name: tu[:name], input: tu[:input], result: tr[:content] }
|
|
249
|
+
}
|
|
250
|
+
end
|
|
251
|
+
|
|
244
252
|
# Send tool results back to the LLM as a user message
|
|
245
253
|
messages << { role: "user", content: tool_results }
|
|
246
254
|
# Exit loop when the LLM signals completion via the "done" tool
|
|
@@ -252,6 +260,15 @@ module Mana
|
|
|
252
260
|
messages << { role: "assistant", content: [{ type: "text", text: "Done: #{done_result}" }] }
|
|
253
261
|
end
|
|
254
262
|
|
|
263
|
+
# Build trace data for external consumers (e.g. Claw::Trace)
|
|
264
|
+
@trace_data = {
|
|
265
|
+
prompt: prompt,
|
|
266
|
+
model: @config.model,
|
|
267
|
+
steps: @_steps,
|
|
268
|
+
total_iterations: iterations,
|
|
269
|
+
timestamp: Time.now.iso8601
|
|
270
|
+
}
|
|
271
|
+
|
|
255
272
|
# Return written variables so Ruby 4.0+ users can capture them:
|
|
256
273
|
# result = ~"compute average and store in <result>"
|
|
257
274
|
# Single write -> return the value directly; multiple -> return Hash.
|
|
@@ -326,7 +343,9 @@ module Mana
|
|
|
326
343
|
vlog("🔄 LLM call ##{@_iteration} → #{@config.model}")
|
|
327
344
|
backend = Backends::Base.for(@config)
|
|
328
345
|
|
|
329
|
-
|
|
346
|
+
t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
347
|
+
|
|
348
|
+
raw = if on_text && backend.respond_to?(:chat_stream)
|
|
330
349
|
backend.chat_stream(
|
|
331
350
|
system: system,
|
|
332
351
|
messages: messages,
|
|
@@ -346,6 +365,15 @@ module Mana
|
|
|
346
365
|
)
|
|
347
366
|
end
|
|
348
367
|
|
|
368
|
+
latency_ms = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - t0) * 1000).round
|
|
369
|
+
|
|
370
|
+
# Backends return { content: [...], usage: {...} }
|
|
371
|
+
result = raw[:content]
|
|
372
|
+
usage = raw[:usage]
|
|
373
|
+
|
|
374
|
+
# Record step for trace
|
|
375
|
+
@_steps << { iteration: @_iteration, usage: usage, latency_ms: latency_ms }
|
|
376
|
+
|
|
349
377
|
result.each do |block|
|
|
350
378
|
type = block[:type] || block["type"]
|
|
351
379
|
case type
|
data/lib/mana/knowledge.rb
CHANGED
|
@@ -179,7 +179,7 @@ module Mana
|
|
|
179
179
|
- Methods on the receiver (minus Ruby builtins) are also discovered.
|
|
180
180
|
- No registration or JSON schema needed — just define normal Ruby methods.
|
|
181
181
|
- LLM-compiled methods: `mana def method_name` lets the LLM generate the implementation
|
|
182
|
-
on first call, then caches it on disk (.
|
|
182
|
+
on first call, then caches it on disk (.ruby-mana/cache/).
|
|
183
183
|
TEXT
|
|
184
184
|
end
|
|
185
185
|
end
|
data/lib/mana/logger.rb
CHANGED
|
@@ -66,16 +66,6 @@ module Mana
|
|
|
66
66
|
end
|
|
67
67
|
end
|
|
68
68
|
|
|
69
|
-
# Log think tool content — full text in distinct italic cyan
|
|
70
|
-
def vlog_think(content)
|
|
71
|
-
return unless @config.verbose
|
|
72
|
-
|
|
73
|
-
$stderr.puts "\e[2m[mana]\e[0m \e[3;36m💭 Think:\e[0m"
|
|
74
|
-
content.each_line do |line|
|
|
75
|
-
$stderr.puts "\e[2m[mana]\e[0m \e[3;36m #{line.rstrip}\e[0m"
|
|
76
|
-
end
|
|
77
|
-
end
|
|
78
|
-
|
|
79
69
|
# Summarize tool input for compact logging.
|
|
80
70
|
# Multi-line string values are replaced with a brief summary.
|
|
81
71
|
def summarize_input(input)
|
data/lib/mana/memory_store.rb
CHANGED
|
@@ -25,7 +25,7 @@ module Mana
|
|
|
25
25
|
|
|
26
26
|
|
|
27
27
|
# Default file-based memory store. Persists memories as JSON files.
|
|
28
|
-
# Storage path resolution: explicit base_path > config.memory_path > {cwd}/.mana
|
|
28
|
+
# Storage path resolution: explicit base_path > config.memory_path > {cwd}/.ruby-mana
|
|
29
29
|
class FileStore < MemoryStore
|
|
30
30
|
# Optional base_path overrides default storage location
|
|
31
31
|
def initialize(base_path = nil)
|
|
@@ -65,15 +65,15 @@ module Mana
|
|
|
65
65
|
end
|
|
66
66
|
|
|
67
67
|
# Resolve the base directory for memory storage.
|
|
68
|
-
# Priority: explicit base_path > config.memory_path > {cwd}/.mana
|
|
68
|
+
# Priority: explicit base_path > config.memory_path > {cwd}/.ruby-mana
|
|
69
69
|
def base_dir
|
|
70
70
|
return File.join(@base_path, "memory") if @base_path
|
|
71
71
|
|
|
72
72
|
custom_path = Mana.config.memory_path
|
|
73
73
|
return File.join(custom_path, "memory") if custom_path
|
|
74
74
|
|
|
75
|
-
# Default fallback — project-local .mana directory
|
|
76
|
-
File.join(Dir.pwd, ".mana")
|
|
75
|
+
# Default fallback — project-local .ruby-mana directory
|
|
76
|
+
File.join(Dir.pwd, ".ruby-mana")
|
|
77
77
|
end
|
|
78
78
|
end
|
|
79
79
|
end
|
data/lib/mana/version.rb
CHANGED
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.13
|
|
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-04-
|
|
11
|
+
date: 2026-04-06 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: binding_of_caller
|