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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0f9221479991a9a07659b6d8aee20d2e0e377f6159e3116fbf01839219d8d011
4
- data.tar.gz: dc2caf733e6a7f4e330599edff45c72557b81205c7b29382237dfa9b286c903b
3
+ metadata.gz: f01dc9677f8c7a7813e0d8df7ec44f0a8fe44c54342d1181f64edd9f34e452a2
4
+ data.tar.gz: 0da4c67c41015a2ce4bb6727a0aeeb2a0334a8f85da9a38f97e122cb294a3169
5
5
  SHA512:
6
- metadata.gz: 3389dabca2ba36ab0e3cf2e5ae2f780f9507eadce0f7551c3ebcf22792003342364cf45dc01bd0315713b8998a2c4be0ec5aede5d13eca2adf493339be6bd038
7
- data.tar.gz: 876a82b19006c6b12822ffaf264d89ac5d81c0e56871d595d7027a72f94c0aa57ec11251ab7458b76338b80fb6f756a700828064d266aaf8084298cc84e77ab5
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 `.mana_cache/` (add to `.gitignore`, or commit them to skip LLM on CI).
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" # directory for memory files
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`, `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.
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
- parsed[:content] || []
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
- content_blocks
71
+ {
72
+ content: content_blocks,
73
+ usage: usage.empty? ? nil : usage
74
+ }
64
75
  end
65
76
  end
66
77
  end
@@ -21,7 +21,10 @@ module Mana
21
21
  }, {
22
22
  "Authorization" => "Bearer #{@config.api_key}"
23
23
  })
24
- normalize_response(parsed)
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
@@ -25,7 +25,7 @@ module Mana
25
25
 
26
26
  # Cache directory for generated .rb files
27
27
  def cache_dir
28
- @cache_dir || ".mana_cache"
28
+ @cache_dir || File.join(".ruby-mana", "cache")
29
29
  end
30
30
 
31
31
  attr_writer :cache_dir
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
- result = if on_text && backend.respond_to?(:chat_stream)
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
@@ -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 (.mana_cache/).
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)
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Mana
4
- VERSION = "0.5.12"
4
+ VERSION = "0.5.13"
5
5
  end
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.12
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-05 00:00:00.000000000 Z
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