lex-llm-ledger 0.4.2 → 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
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,22 @@
|
|
|
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
|
+
|
|
3
20
|
## [0.4.2] - 2026-05-22
|
|
4
21
|
|
|
5
22
|
### Fixed
|
|
@@ -10,8 +10,6 @@ module Legion
|
|
|
10
10
|
module Extensions
|
|
11
11
|
module Llm
|
|
12
12
|
module Ledger
|
|
13
|
-
class ResponseNotReady < StandardError; end
|
|
14
|
-
|
|
15
13
|
module Runners
|
|
16
14
|
module Tools
|
|
17
15
|
extend self
|
|
@@ -31,11 +29,13 @@ module Legion
|
|
|
31
29
|
)
|
|
32
30
|
|
|
33
31
|
db = ::Legion::Data.connection
|
|
32
|
+
response = find_or_resolve_response_with_retry(db, body, ctx, props, headers)
|
|
34
33
|
write_result = [:ok]
|
|
35
34
|
db.transaction do
|
|
36
|
-
response = find_or_resolve_response(db, body, ctx, props, headers)
|
|
37
35
|
identity_attrs = extract_identity_attrs(body, headers, db)
|
|
38
|
-
|
|
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)
|
|
39
39
|
if tool_call_row && !new_tool_call
|
|
40
40
|
write_result[0] = :duplicate
|
|
41
41
|
elsif new_tool_call
|
|
@@ -53,9 +53,6 @@ module Legion
|
|
|
53
53
|
rescue Helpers::DecryptionFailed => e
|
|
54
54
|
handle_exception(e, level: :error, handled: true, operation: 'write_tool_record.decrypt')
|
|
55
55
|
raise
|
|
56
|
-
rescue ResponseNotReady => e
|
|
57
|
-
log.warn("[ledger] write_tool_record: parent response not yet written, dead-lettering message (#{e.message})")
|
|
58
|
-
raise unrecoverable_message_error(e)
|
|
59
56
|
rescue StandardError => e
|
|
60
57
|
handle_exception(e, level: :error, handled: true, operation: 'write_tool_record')
|
|
61
58
|
{ result: :error, error: e.message }
|
|
@@ -67,16 +64,26 @@ module Legion
|
|
|
67
64
|
Helpers::SubscriptionMessage.runner_args(payload, metadata, message)
|
|
68
65
|
end
|
|
69
66
|
|
|
70
|
-
def
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
|
75
81
|
end
|
|
82
|
+
|
|
83
|
+
log.info('[ledger] write_tool_record: response not available after retries, proceeding with null response_id')
|
|
84
|
+
nil
|
|
76
85
|
end
|
|
77
86
|
|
|
78
|
-
# Resolve the llm_message_inference_responses row this tool call belongs to.
|
|
79
|
-
# Returns nil if we cannot link at all.
|
|
80
87
|
def find_or_resolve_response(db, body, ctx, props, headers)
|
|
81
88
|
request_ref = ctx[:request_id] || body[:request_id] ||
|
|
82
89
|
props[:correlation_id] || headers['x-legion-llm-request-id']
|
|
@@ -89,17 +96,30 @@ module Legion
|
|
|
89
96
|
.where(message_inference_request_id: request[:id]).first
|
|
90
97
|
end
|
|
91
98
|
|
|
92
|
-
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)
|
|
93
110
|
tool_uuid = derive_tool_call_uuid(body, ctx, tool, headers)
|
|
94
111
|
existing = db[:llm_tool_calls].where(uuid: tool_uuid).first
|
|
95
112
|
return [existing, false] if existing # rubocop:disable Legion/Extension/RunnerReturnHash
|
|
96
113
|
|
|
97
|
-
response_id =
|
|
98
|
-
return [nil, false] unless response_id # rubocop:disable Legion/Extension/RunnerReturnHash
|
|
114
|
+
response_id = response&.[](:id)
|
|
99
115
|
|
|
100
|
-
next_index =
|
|
101
|
-
|
|
102
|
-
|
|
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
|
|
103
123
|
|
|
104
124
|
src = tool[:source] || {}
|
|
105
125
|
status = tool[:status] || headers['x-legion-tool-status'] || 'success'
|
|
@@ -108,6 +128,7 @@ module Legion
|
|
|
108
128
|
id = insert_with_savepoint(db, :llm_tool_calls, {
|
|
109
129
|
uuid: tool_uuid,
|
|
110
130
|
message_inference_response_id: response_id,
|
|
131
|
+
conversation_id: conversation_id,
|
|
111
132
|
tool_call_index: next_index,
|
|
112
133
|
provider_tool_call_ref: tool[:id],
|
|
113
134
|
tool_name: tool[:name] || headers['x-legion-tool-name'],
|
|
@@ -128,34 +149,6 @@ module Legion
|
|
|
128
149
|
[row, false]
|
|
129
150
|
end
|
|
130
151
|
|
|
131
|
-
# Extract or fall back to find a response_id for linking the tool call.
|
|
132
|
-
def resolve_response_id(db, response, body, ctx, headers, tool_uuid)
|
|
133
|
-
return response[:id] if response # rubocop:disable Legion/Extension/RunnerReturnHash
|
|
134
|
-
|
|
135
|
-
fallback = fallback_response_for_conversation(db, body, ctx, headers)
|
|
136
|
-
return fallback[:id] if fallback # rubocop:disable Legion/Extension/RunnerReturnHash
|
|
137
|
-
|
|
138
|
-
raise ResponseNotReady, "no response row found for tool call uuid=#{tool_uuid}"
|
|
139
|
-
end
|
|
140
|
-
|
|
141
|
-
def fallback_response_for_conversation(db, body, ctx, headers)
|
|
142
|
-
conv_id = ctx[:conversation_id] || body[:conversation_id] ||
|
|
143
|
-
headers['x-legion-llm-conversation-id']
|
|
144
|
-
return nil unless conv_id # rubocop:disable Legion/Extension/RunnerReturnHash
|
|
145
|
-
|
|
146
|
-
conv = db[:llm_conversations].where(uuid: stable_uuid(conv_id)).first ||
|
|
147
|
-
db[:llm_conversations].where(uuid: conv_id).first
|
|
148
|
-
return nil unless conv # rubocop:disable Legion/Extension/RunnerReturnHash
|
|
149
|
-
|
|
150
|
-
db[:llm_message_inference_responses]
|
|
151
|
-
.join(:llm_message_inference_requests,
|
|
152
|
-
id: :message_inference_request_id)
|
|
153
|
-
.where(Sequel[:llm_message_inference_requests][:conversation_id] => conv[:id])
|
|
154
|
-
.order(Sequel.desc(Sequel[:llm_message_inference_responses][:id]))
|
|
155
|
-
.select_all(:llm_message_inference_responses)
|
|
156
|
-
.first
|
|
157
|
-
end
|
|
158
|
-
|
|
159
152
|
def find_or_create_tool_call_attempt(db, tool_call_row, tool, body, props, headers, identity_attrs) # rubocop:disable Metrics/CyclomaticComplexity
|
|
160
153
|
return nil unless tool_call_row # rubocop:disable Legion/Extension/RunnerReturnHash
|
|
161
154
|
|
|
@@ -265,6 +258,12 @@ module Legion
|
|
|
265
258
|
"#{hex[0, 8]}-#{hex[8, 4]}-#{hex[12, 4]}-#{hex[16, 4]}-#{hex[20, 12]}"
|
|
266
259
|
end
|
|
267
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
|
+
|
|
268
267
|
def insert_with_savepoint(db, table, attributes, operation:)
|
|
269
268
|
db.transaction(savepoint: true) do
|
|
270
269
|
Helpers::PersistenceLogging.insert_row(db, table, attributes,
|