lex-llm 0.6.3 → 0.6.8

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: b1d6f2214c5ae415eab2e51068984dfdfb3cc7a35223439573d550d820bf864d
4
- data.tar.gz: 23ba380421192e5320c2060fd89e357d26d2cb7958836697970eaa4bba5746fc
3
+ metadata.gz: 5613f655c5743be588ec5030535de96dc998cb8f03ffb8f8b2be21c95dc55b84
4
+ data.tar.gz: cba342c0f0662d7612b2d8647791149ddb716878e79e487e84a640dbcd3582cc
5
5
  SHA512:
6
- metadata.gz: e1fb762ee7588f593a75e81b3a6ebc15f131ba74d0d5a54a6cd030d0367c4e41a1a2240c4287c10de4b7bbf25838f27c7a4ae557f75c67aa0b1e481378f752d8
7
- data.tar.gz: 9d29438b9d78732887e081b41b7ee792b2459284d654e80e7c70f12be62fcaa585c3e166213530d7bfa57fd09bc5b9de43e12530daf8a38574172452de30a727
6
+ metadata.gz: b2eb4840e8f1f39d65690dc9226387d6fecbcc9352845cc796d2421b56b660fddae35e2373abdbb88e5c3b127a5b5a6ba621c33682762bbf2eeb271809ec2beb
7
+ data.tar.gz: 7cce383baa60095d03479240c16076624706d56d605200efee5943d3de290ba96c5c18c58e7ada252572966657d846107a3f55945d9f95aa278f553c31ac82a2
data/CHANGELOG.md CHANGED
@@ -1,5 +1,36 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.6.8 - 2026-07-03
4
+
5
+ ### Changed
6
+ - Version bump to publish the `MeteringFlush` actor (0.6.5). Versions 0.6.5–0.6.7 tagged and created GitHub releases but never reached RubyGems: 0.6.5/0.6.6 failed `gem push` on a revoked API key, and 0.6.7 failed because the replacement API key was created with per-key MFA enabled (`gem push` prompted for an OTP a CI runner can't provide). The API key has been recreated without per-key MFA. Because the release workflow skips publishing when a version's tag/release already exists, each failed version must be superseded by a fresh one. No functional changes since 0.6.5.
7
+
8
+ ## 0.6.7 - 2026-07-03
9
+
10
+ ### Changed
11
+ - Version bump attempting to publish the `MeteringFlush` actor. Did not reach RubyGems — the replacement API key carried a per-key MFA requirement, so `gem push` demanded an OTP (superseded by 0.6.8). No functional changes.
12
+
13
+ ## 0.6.6 - 2026-07-03
14
+
15
+ ### Changed
16
+ - Version bump attempting to re-trigger the release pipeline. Did not publish to RubyGems — the `gem push` step failed on a revoked API key (superseded by 0.6.7). No functional changes.
17
+
18
+ ## 0.6.5 - 2026-07-03
19
+
20
+ ### Added
21
+ - **`MeteringFlush` actor drains the LLM metering spool back into RabbitMQ.** legion-llm's `Legion::LLM::Metering` spools metering events to `~/.legionio/data/spool/metering/events.jsonl` whenever transport is down at emit time. Nothing had been draining that spool since `lex-llm-gateway` was retired, so events (chat, embeddings, skills — all funnel through `Legion::LLM::Metering.emit`) accumulated indefinitely. This `Legion::Extensions::Actors::Every` actor ticks every 60s and calls `Legion::LLM::Metering.flush_spool`, which republishes each spooled event to the `llm.metering` exchange and truncates the file. It is a no-op while transport is unavailable, so it is safe to tick continuously. Runs on every node (not a singleton) because each node owns its own spool file and must drain it locally. References legion-llm by string (`'Legion::LLM::Metering'`) so lex-llm — the lower gem — avoids a circular dependency; the constant is resolved at tick time.
22
+
23
+ ## 0.6.4 - 2026-06-30
24
+
25
+ ### Fixed
26
+ - **Canonical Data structs now serialize correctly via MultiJson/Oj/::JSON** — all 10 `Data.define` canonical structs (`ToolCall`, `Message`, `ContentBlock`, `Chunk`, `Params`, `Request`, `Response`, `Thinking`, `ToolDefinition`, `Usage`) now implement `as_json` and `to_json`, delegating to their existing `#to_h` method. Without these callbacks, `MultiJson.dump` (and any JSON encoder) fell back to `obj.to_s` on canonical structs, producing the Ruby `#inspect` dump (e.g. `#<data Legion::Extensions::Llm::Canonical::ToolCall id="toolu_bdrk_...", ...>`) wherever a struct appeared inside a Hash/Array being serialized. This leaked into:
27
+ - Client responses (`/v1/chat/completions`, `/v1/messages`, `/v1/responses`) — `tool_calls` in assistant message history appeared as inspect strings instead of structured JSON objects, breaking LangGraph/Freelens supervisors that expect structured routing decisions.
28
+ - Ledger persistence (`llm_message_inference_requests.request_json`) — tool_calls stored as unparseable Ruby inspect strings, breaking history reconstruction on subsequent turns.
29
+ - AMQP wire payloads — any consumer receiving a message containing canonical structs saw inspect strings instead of structured data.
30
+ - Debug echo-request (`X-Legion-Debug: echo-request`) — canonical request snapshot contained inspect strings for any `tool_calls` in message history.
31
+
32
+ The fix is structural: canonical structs now self-enforce correct JSON serialization at the architectural boundary (per Amendment A of the N×N routing design), so every downstream consumer — ledger, AMQP publisher, client translator, debug formatter — serializes correctly without needing to call `.to_h` explicitly.
33
+
3
34
  ## 0.6.3 - 2026-06-25
4
35
 
5
36
  ### Fixed
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Llm
6
+ module Actor
7
+ # Periodically drains the LLM metering spool.
8
+ #
9
+ # Metering events are emitted by legion-llm's Legion::LLM::Metering. When
10
+ # transport is down at emit time they are appended to a durable JSONL
11
+ # spool (~/.legionio/data/spool/metering/events.jsonl) instead of being
12
+ # published to the `llm.metering` exchange. Nothing was draining that
13
+ # spool after lex-llm-gateway was retired, so events accumulated forever.
14
+ #
15
+ # flush_spool reads every spooled event — chat, embeddings, skills, all
16
+ # funnel through Legion::LLM::Metering.emit into the same file — and
17
+ # republishes each to the exchange, then truncates. It is a no-op when
18
+ # transport is unavailable, so it is safe to tick every minute.
19
+ #
20
+ # runner_class points at legion-llm (the higher gem, always loaded at
21
+ # full boot). lex-llm must not require legion-llm — that would be circular
22
+ # (legion-llm depends on lex-llm) — so we reference it by string and let
23
+ # the actor base resolve the constant at tick time via const_get.
24
+ #
25
+ # NOT a singleton actor: every node runs LegionIO independently and owns
26
+ # its own spool file, so each node must drain its own spool. Leader
27
+ # election would strand every non-leader node's spool.
28
+ class MeteringFlush < Legion::Extensions::Actors::Every
29
+ def runner_class
30
+ 'Legion::LLM::Metering'
31
+ end
32
+
33
+ def runner_function
34
+ 'flush_spool'
35
+ end
36
+
37
+ def time
38
+ 60
39
+ end
40
+
41
+ def run_now?
42
+ false
43
+ end
44
+
45
+ def use_runner?
46
+ false
47
+ end
48
+
49
+ def check_subtask?
50
+ false
51
+ end
52
+
53
+ def generate_task?
54
+ false
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -162,6 +162,15 @@ module Legion
162
162
  }.compact
163
163
  end
164
164
 
165
+ # MultiJson/Oj/::JSON callback — prevents Data.define #inspect leak into JSON.
166
+ def as_json(*)
167
+ to_h
168
+ end
169
+
170
+ def to_json(*)
171
+ to_h.to_json(*)
172
+ end
173
+
165
174
  # Type predicate helpers.
166
175
  def text_delta? = type == :text_delta
167
176
  def thinking_delta? = type == :thinking_delta
@@ -110,6 +110,15 @@ module Legion
110
110
  super.compact
111
111
  end
112
112
 
113
+ # MultiJson/Oj/::JSON callback — prevents Data.define #inspect leak into JSON.
114
+ def as_json(*)
115
+ to_h
116
+ end
117
+
118
+ def to_json(*)
119
+ to_h.to_json(*)
120
+ end
121
+
113
122
  # Human-readable string — prevents #inspect leaking into user-facing output.
114
123
  def to_s
115
124
  return "[tool_use:#{name}]" if type == :tool_use
@@ -123,6 +123,15 @@ module Legion
123
123
  super.compact
124
124
  end
125
125
 
126
+ # MultiJson/Oj/::JSON callback — prevents Data.define #inspect leak into JSON.
127
+ def as_json(*)
128
+ to_h
129
+ end
130
+
131
+ def to_json(*)
132
+ to_h.to_json(*)
133
+ end
134
+
126
135
  # Human-readable string — prevents #inspect leaking into user-facing output.
127
136
  def to_s
128
137
  text
@@ -53,6 +53,15 @@ module Legion
53
53
  def to_h
54
54
  super.compact
55
55
  end
56
+
57
+ # MultiJson/Oj/::JSON callback — prevents Data.define #inspect leak into JSON.
58
+ def as_json(*)
59
+ to_h
60
+ end
61
+
62
+ def to_json(*)
63
+ to_h.to_json(*)
64
+ end
56
65
  end
57
66
  # rubocop:enable Lint/ConstantDefinitionInBlock
58
67
  end
@@ -92,6 +92,15 @@ module Legion
92
92
  }.compact
93
93
  end
94
94
 
95
+ # MultiJson/Oj/::JSON callback — prevents Data.define #inspect leak into JSON.
96
+ def as_json(*)
97
+ to_h
98
+ end
99
+
100
+ def to_json(*)
101
+ to_h.to_json(*)
102
+ end
103
+
95
104
  def self.normalize_tools(tools)
96
105
  return {} if tools.nil? || tools.empty?
97
106
 
@@ -104,6 +104,15 @@ module Legion
104
104
  end
105
105
  end
106
106
 
107
+ # MultiJson/Oj/::JSON callback — prevents Data.define #inspect leak into JSON.
108
+ def as_json(*)
109
+ to_h
110
+ end
111
+
112
+ def to_json(*)
113
+ to_h.to_json(*)
114
+ end
115
+
107
116
  # Whether the response includes tool calls.
108
117
  def tool_call?
109
118
  !tool_calls.nil? && !tool_calls.empty?
@@ -31,6 +31,15 @@ module Legion
31
31
  super.compact
32
32
  end
33
33
 
34
+ # MultiJson/Oj/::JSON callback — prevents Data.define #inspect leak into JSON.
35
+ def as_json(*)
36
+ to_h
37
+ end
38
+
39
+ def to_json(*)
40
+ to_h.to_json(*)
41
+ end
42
+
34
43
  # Whether this thinking block has any content.
35
44
  def empty?
36
45
  content.nil? && signature.nil?
@@ -106,6 +106,16 @@ module Legion
106
106
  super.compact
107
107
  end
108
108
 
109
+ # MultiJson/Oj/::JSON callback for unknown types — without this, fallback is
110
+ # obj.to_s which for Data.define returns the #inspect dump and leaks into JSON.
111
+ def as_json(*)
112
+ to_h
113
+ end
114
+
115
+ def to_json(*)
116
+ to_h.to_json(*)
117
+ end
118
+
109
119
  # Subset for audit/ledger emission.
110
120
  def to_audit_hash
111
121
  {
@@ -91,6 +91,15 @@ module Legion
91
91
  parameters: parameters
92
92
  }.compact.reject { |k, v| k == :description && v == '' }
93
93
  end
94
+
95
+ # MultiJson/Oj/::JSON callback — prevents Data.define #inspect leak into JSON.
96
+ def as_json(*)
97
+ to_h
98
+ end
99
+
100
+ def to_json(*)
101
+ to_h.to_json(*)
102
+ end
94
103
  end
95
104
  end
96
105
  end
@@ -61,6 +61,15 @@ module Legion
61
61
  super.compact
62
62
  end
63
63
 
64
+ # MultiJson/Oj/::JSON callback — prevents Data.define #inspect leak into JSON.
65
+ def as_json(*)
66
+ to_h
67
+ end
68
+
69
+ def to_json(*)
70
+ to_h.to_json(*)
71
+ end
72
+
64
73
  # Total tokens across all categories.
65
74
  def total_tokens
66
75
  [input_tokens, output_tokens, cache_read_tokens, cache_write_tokens,
@@ -3,7 +3,7 @@
3
3
  module Legion
4
4
  module Extensions
5
5
  module Llm
6
- VERSION = '0.6.3'
6
+ VERSION = '0.6.8'
7
7
  end
8
8
  end
9
9
  end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Stub the actor base class before requiring the real actor — the concrete
4
+ # Legion::Extensions::Actors::Every lives in LegionIO, which lex-llm does not
5
+ # depend on (lex-llm is the lower gem).
6
+ unless defined?(Legion::Extensions::Actors::Every)
7
+ module Legion
8
+ module Extensions
9
+ module Actors
10
+ class Every; end # rubocop:disable Lint/EmptyClass
11
+ end
12
+ end
13
+ end
14
+ end
15
+
16
+ $LOADED_FEATURES << 'legion/extensions/actors/every'
17
+
18
+ require_relative '../../../../../lib/legion/extensions/llm/actors/metering_flush'
19
+
20
+ RSpec.describe Legion::Extensions::Llm::Actor::MeteringFlush do
21
+ subject(:actor) { described_class.new }
22
+
23
+ describe '#runner_class' do
24
+ it 'targets the legion-llm metering module that owns the spool' do
25
+ expect(actor.runner_class).to eq('Legion::LLM::Metering')
26
+ end
27
+ end
28
+
29
+ describe '#runner_function' do
30
+ it 'returns flush_spool' do
31
+ expect(actor.runner_function).to eq('flush_spool')
32
+ end
33
+ end
34
+
35
+ describe '#time' do
36
+ it 'flushes once per minute' do
37
+ expect(actor.time).to eq(60)
38
+ end
39
+ end
40
+
41
+ describe '#run_now?' do
42
+ it 'returns false so it does not fire during boot' do
43
+ expect(actor.run_now?).to be false
44
+ end
45
+ end
46
+
47
+ describe '#use_runner?' do
48
+ it 'returns false so it calls the module directly (no AMQP round-trip)' do
49
+ expect(actor.use_runner?).to be false
50
+ end
51
+ end
52
+
53
+ describe '#check_subtask?' do
54
+ it 'returns false' do
55
+ expect(actor.check_subtask?).to be false
56
+ end
57
+ end
58
+
59
+ describe '#generate_task?' do
60
+ it 'returns false' do
61
+ expect(actor.generate_task?).to be false
62
+ end
63
+ end
64
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lex-llm
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.3
4
+ version: 0.6.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - LegionIO
@@ -255,6 +255,7 @@ files:
255
255
  - README.md
256
256
  - lex-llm.gemspec
257
257
  - lib/legion/extensions/llm.rb
258
+ - lib/legion/extensions/llm/actors/metering_flush.rb
258
259
  - lib/legion/extensions/llm/agent.rb
259
260
  - lib/legion/extensions/llm/aliases.json
260
261
  - lib/legion/extensions/llm/aliases.rb
@@ -347,6 +348,7 @@ files:
347
348
  - spec/fixtures/ruby.wav
348
349
  - spec/fixtures/ruby.xml
349
350
  - spec/fixtures/sample.pdf
351
+ - spec/legion/extensions/llm/actor/metering_flush_spec.rb
350
352
  - spec/legion/extensions/llm/agent_spec.rb
351
353
  - spec/legion/extensions/llm/attachment_spec.rb
352
354
  - spec/legion/extensions/llm/auto_registration_spec.rb