lex-llm-ledger 0.4.0 → 0.5.0
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 +30 -0
- data/lib/legion/extensions/llm/ledger/actors/registry_availability.rb +2 -0
- data/lib/legion/extensions/llm/ledger/runners/tools.rb +50 -39
- data/lib/legion/extensions/llm/ledger/version.rb +1 -1
- data/lib/legion/extensions/llm/ledger.rb +5 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d7432d720f451a54b2826adc2eb2fa6335efb0d05cf993d2b84d29c4048090d4
|
|
4
|
+
data.tar.gz: a77af8bd22c3e4438b5a2c1e5ba684eb042b9612aff691fcc7085a5078ee752a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0b03a48cfd7912f193ce78263d44cc1ca996f5d8289f971f836a9e8231b2630bd61d4ece62d1489c8fdc92414174e4e6275df088585d0a2763496e7ff673cff5
|
|
7
|
+
data.tar.gz: 287814a7495a4f10ba0a95f4b2f2206fadbc13bd3b6143173477dccf276ae2d15658b88e273e94abed67d0734eb9d91fee085c7b088bf7a8705e09b9e4b8e03e
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,35 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.5.0] - 2026-05-26
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
- Tool audit writes no longer dead-letter when the parent response row is missing. The runner retries up to 3 times (1s delay each, configurable via `tool_write` settings), then inserts with a NULL `message_inference_response_id` instead of raising `UnrecoverableMessageError`.
|
|
7
|
+
- Removed `ResponseNotReady` exception class — tool calls are always persisted now.
|
|
8
|
+
- Populate `conversation_id` FK on `llm_tool_calls` from the message payload/headers, providing conversation-level traceability even when the response FK is NULL.
|
|
9
|
+
- Retry configuration moved to `default_settings[:tool_write]` (`response_retry_attempts`, `response_retry_delay`) — tunable at runtime without code changes.
|
|
10
|
+
|
|
11
|
+
### Requires
|
|
12
|
+
- legion-data >= 1.8.9 (migrations 116-117)
|
|
13
|
+
|
|
14
|
+
## [0.4.3] - 2026-05-22
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
- Persist `llm.registry.availability` publisher identity from current transport headers into `llm_registry_availability_records`, including best-effort `identity_principal_id` and `identity_id` from DB id headers.
|
|
18
|
+
- Preserve legacy identity header and body fallbacks for registry availability records when current transport identity headers are absent.
|
|
19
|
+
|
|
20
|
+
## [0.4.2] - 2026-05-22
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
- Dead-letter tool audit messages with missing parent response rows via `UnrecoverableMessageError` so the subscription rejects the RabbitMQ delivery with `requeue: false` instead of acknowledging, republishing, or blocking inside a runner-local sleep/retry loop.
|
|
24
|
+
- Set the `llm.registry.availability` subscription actor prefetch to 4 so registry availability events can drain with modest concurrency.
|
|
25
|
+
|
|
26
|
+
## [0.4.1] - 2026-05-18
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
- Tool write retries once after 1s when parent response row is not yet committed (race between async metering publish and tool audit AMQP delivery)
|
|
30
|
+
- Raises `ResponseNotReady` instead of silently returning nil when response row is missing
|
|
31
|
+
|
|
32
|
+
|
|
3
33
|
## [0.4.0] - 2026-05-17
|
|
4
34
|
|
|
5
35
|
### Changed
|
|
@@ -29,11 +29,13 @@ module Legion
|
|
|
29
29
|
)
|
|
30
30
|
|
|
31
31
|
db = ::Legion::Data.connection
|
|
32
|
+
response = find_or_resolve_response_with_retry(db, body, ctx, props, headers)
|
|
32
33
|
write_result = [:ok]
|
|
33
34
|
db.transaction do
|
|
34
|
-
response = find_or_resolve_response(db, body, ctx, props, headers)
|
|
35
35
|
identity_attrs = extract_identity_attrs(body, headers, db)
|
|
36
|
-
|
|
36
|
+
conversation_id = resolve_conversation_id(db, body, ctx, headers)
|
|
37
|
+
tool_call_row, new_tool_call = find_or_create_tool_call(db, response, body, ctx, tool, headers,
|
|
38
|
+
identity_attrs, conversation_id)
|
|
37
39
|
if tool_call_row && !new_tool_call
|
|
38
40
|
write_result[0] = :duplicate
|
|
39
41
|
elsif new_tool_call
|
|
@@ -62,8 +64,26 @@ module Legion
|
|
|
62
64
|
Helpers::SubscriptionMessage.runner_args(payload, metadata, message)
|
|
63
65
|
end
|
|
64
66
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
+
def find_or_resolve_response_with_retry(db, body, ctx, props, headers)
|
|
68
|
+
response = find_or_resolve_response(db, body, ctx, props, headers)
|
|
69
|
+
return response if response # rubocop:disable Legion/Extension/RunnerReturnHash
|
|
70
|
+
|
|
71
|
+
retry_attempts = tool_write_setting(:response_retry_attempts, 3)
|
|
72
|
+
retry_delay = tool_write_setting(:response_retry_delay, 1)
|
|
73
|
+
|
|
74
|
+
retry_attempts.times do |attempt|
|
|
75
|
+
sleep retry_delay
|
|
76
|
+
response = find_or_resolve_response(db, body, ctx, props, headers)
|
|
77
|
+
if response
|
|
78
|
+
log.debug("[ledger] write_tool_record: response found on retry #{attempt + 1}")
|
|
79
|
+
return response # rubocop:disable Legion/Extension/RunnerReturnHash
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
log.info('[ledger] write_tool_record: response not available after retries, proceeding with null response_id')
|
|
84
|
+
nil
|
|
85
|
+
end
|
|
86
|
+
|
|
67
87
|
def find_or_resolve_response(db, body, ctx, props, headers)
|
|
68
88
|
request_ref = ctx[:request_id] || body[:request_id] ||
|
|
69
89
|
props[:correlation_id] || headers['x-legion-llm-request-id']
|
|
@@ -76,17 +96,30 @@ module Legion
|
|
|
76
96
|
.where(message_inference_request_id: request[:id]).first
|
|
77
97
|
end
|
|
78
98
|
|
|
79
|
-
def
|
|
99
|
+
def resolve_conversation_id(db, body, ctx, headers)
|
|
100
|
+
conv_ref = ctx[:conversation_id] || body[:conversation_id] ||
|
|
101
|
+
headers['x-legion-llm-conversation-id']
|
|
102
|
+
return nil unless conv_ref # rubocop:disable Legion/Extension/RunnerReturnHash
|
|
103
|
+
|
|
104
|
+
conv = db[:llm_conversations].where(uuid: stable_uuid(conv_ref)).first ||
|
|
105
|
+
db[:llm_conversations].where(uuid: conv_ref).first
|
|
106
|
+
conv&.[](:id)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def find_or_create_tool_call(db, response, body, ctx, tool, headers, identity_attrs, conversation_id)
|
|
80
110
|
tool_uuid = derive_tool_call_uuid(body, ctx, tool, headers)
|
|
81
111
|
existing = db[:llm_tool_calls].where(uuid: tool_uuid).first
|
|
82
112
|
return [existing, false] if existing # rubocop:disable Legion/Extension/RunnerReturnHash
|
|
83
113
|
|
|
84
|
-
response_id =
|
|
85
|
-
return [nil, false] unless response_id # rubocop:disable Legion/Extension/RunnerReturnHash
|
|
114
|
+
response_id = response&.[](:id)
|
|
86
115
|
|
|
87
|
-
next_index =
|
|
88
|
-
|
|
89
|
-
|
|
116
|
+
next_index = if response_id
|
|
117
|
+
db[:llm_tool_calls]
|
|
118
|
+
.where(message_inference_response_id: response_id)
|
|
119
|
+
.max(:tool_call_index).to_i + 1
|
|
120
|
+
else
|
|
121
|
+
0
|
|
122
|
+
end
|
|
90
123
|
|
|
91
124
|
src = tool[:source] || {}
|
|
92
125
|
status = tool[:status] || headers['x-legion-tool-status'] || 'success'
|
|
@@ -95,6 +128,7 @@ module Legion
|
|
|
95
128
|
id = insert_with_savepoint(db, :llm_tool_calls, {
|
|
96
129
|
uuid: tool_uuid,
|
|
97
130
|
message_inference_response_id: response_id,
|
|
131
|
+
conversation_id: conversation_id,
|
|
98
132
|
tool_call_index: next_index,
|
|
99
133
|
provider_tool_call_ref: tool[:id],
|
|
100
134
|
tool_name: tool[:name] || headers['x-legion-tool-name'],
|
|
@@ -115,35 +149,6 @@ module Legion
|
|
|
115
149
|
[row, false]
|
|
116
150
|
end
|
|
117
151
|
|
|
118
|
-
# Extract or fall back to find a response_id for linking the tool call.
|
|
119
|
-
def resolve_response_id(db, response, body, ctx, headers, tool_uuid)
|
|
120
|
-
return response[:id] if response # rubocop:disable Legion/Extension/RunnerReturnHash
|
|
121
|
-
|
|
122
|
-
fallback = fallback_response_for_conversation(db, body, ctx, headers)
|
|
123
|
-
return fallback[:id] if fallback # rubocop:disable Legion/Extension/RunnerReturnHash
|
|
124
|
-
|
|
125
|
-
log.warn("[ledger] write_tool_record: no response row found for tool call uuid=#{tool_uuid}, skipping")
|
|
126
|
-
nil
|
|
127
|
-
end
|
|
128
|
-
|
|
129
|
-
def fallback_response_for_conversation(db, body, ctx, headers)
|
|
130
|
-
conv_id = ctx[:conversation_id] || body[:conversation_id] ||
|
|
131
|
-
headers['x-legion-llm-conversation-id']
|
|
132
|
-
return nil unless conv_id # rubocop:disable Legion/Extension/RunnerReturnHash
|
|
133
|
-
|
|
134
|
-
conv = db[:llm_conversations].where(uuid: stable_uuid(conv_id)).first ||
|
|
135
|
-
db[:llm_conversations].where(uuid: conv_id).first
|
|
136
|
-
return nil unless conv # rubocop:disable Legion/Extension/RunnerReturnHash
|
|
137
|
-
|
|
138
|
-
db[:llm_message_inference_responses]
|
|
139
|
-
.join(:llm_message_inference_requests,
|
|
140
|
-
id: :message_inference_request_id)
|
|
141
|
-
.where(Sequel[:llm_message_inference_requests][:conversation_id] => conv[:id])
|
|
142
|
-
.order(Sequel.desc(Sequel[:llm_message_inference_responses][:id]))
|
|
143
|
-
.select_all(:llm_message_inference_responses)
|
|
144
|
-
.first
|
|
145
|
-
end
|
|
146
|
-
|
|
147
152
|
def find_or_create_tool_call_attempt(db, tool_call_row, tool, body, props, headers, identity_attrs) # rubocop:disable Metrics/CyclomaticComplexity
|
|
148
153
|
return nil unless tool_call_row # rubocop:disable Legion/Extension/RunnerReturnHash
|
|
149
154
|
|
|
@@ -253,6 +258,12 @@ module Legion
|
|
|
253
258
|
"#{hex[0, 8]}-#{hex[8, 4]}-#{hex[12, 4]}-#{hex[16, 4]}-#{hex[20, 12]}"
|
|
254
259
|
end
|
|
255
260
|
|
|
261
|
+
def tool_write_setting(key, default)
|
|
262
|
+
ledger = Legion::Settings.dig(:extensions, :llm, :ledger) || {}
|
|
263
|
+
tool_write = ledger[:tool_write] || {}
|
|
264
|
+
(tool_write[key] || default).to_i
|
|
265
|
+
end
|
|
266
|
+
|
|
256
267
|
def insert_with_savepoint(db, table, attributes, operation:)
|
|
257
268
|
db.transaction(savepoint: true) do
|
|
258
269
|
Helpers::PersistenceLogging.insert_row(db, table, attributes,
|