lex-llm 0.6.16 → 0.7.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.
Files changed (60) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +14 -0
  3. data/lib/legion/extensions/llm/auto_registration.rb +3 -0
  4. data/lib/legion/extensions/llm/fleet/protocol.rb +12 -0
  5. data/lib/legion/extensions/llm/fleet/provider_responder.rb +40 -5
  6. data/lib/legion/extensions/llm/fleet/token_validator.rb +15 -0
  7. data/lib/legion/extensions/llm/fleet/worker_execution.rb +166 -2
  8. data/lib/legion/extensions/llm/inventory/callable_handle.rb +144 -0
  9. data/lib/legion/extensions/llm/inventory/errors.rb +34 -0
  10. data/lib/legion/extensions/llm/inventory/evidence.rb +134 -0
  11. data/lib/legion/extensions/llm/inventory/identity.rb +119 -0
  12. data/lib/legion/extensions/llm/inventory/immutable_value.rb +96 -0
  13. data/lib/legion/extensions/llm/inventory/probe_coordinator.rb +117 -0
  14. data/lib/legion/extensions/llm/inventory/probe_token.rb +146 -0
  15. data/lib/legion/extensions/llm/inventory/publisher.rb +110 -0
  16. data/lib/legion/extensions/llm/inventory/records.rb +592 -0
  17. data/lib/legion/extensions/llm/inventory/registry.rb +636 -0
  18. data/lib/legion/extensions/llm/inventory/scoped_refresher.rb +206 -0
  19. data/lib/legion/extensions/llm/inventory/snapshot.rb +91 -0
  20. data/lib/legion/extensions/llm/provider.rb +43 -0
  21. data/lib/legion/extensions/llm/routing/lane_key.rb +4 -0
  22. data/lib/legion/extensions/llm/routing/model_offering.rb +3 -0
  23. data/lib/legion/extensions/llm/routing/offering_registry.rb +2 -0
  24. data/lib/legion/extensions/llm/routing/provider_outcome.rb +54 -0
  25. data/lib/legion/extensions/llm/routing/records.rb +288 -0
  26. data/lib/legion/extensions/llm/taxonomies.rb +90 -0
  27. data/lib/legion/extensions/llm/transport/messages/fleet_error.rb +3 -1
  28. data/lib/legion/extensions/llm/transport/messages/fleet_response.rb +3 -1
  29. data/lib/legion/extensions/llm/version.rb +1 -1
  30. data/lib/legion/extensions/llm.rb +15 -0
  31. data/spec/legion/extensions/llm/conformance/fixtures/ssot_identity_vectors.json +84 -0
  32. data/spec/legion/extensions/llm/conformance/ssot_provider_conformance_spec.rb +13 -0
  33. data/spec/legion/extensions/llm/conformance/ssot_provider_examples.rb +196 -0
  34. data/spec/legion/extensions/llm/fleet/exact_offering_spec.rb +179 -0
  35. data/spec/legion/extensions/llm/fleet/provider_responder_spec.rb +26 -0
  36. data/spec/legion/extensions/llm/fleet_messages_spec.rb +14 -0
  37. data/spec/legion/extensions/llm/inventory/boot_spec.rb +80 -0
  38. data/spec/legion/extensions/llm/inventory/callable_handle_spec.rb +127 -0
  39. data/spec/legion/extensions/llm/inventory/evidence_spec.rb +125 -0
  40. data/spec/legion/extensions/llm/inventory/identity_spec.rb +227 -0
  41. data/spec/legion/extensions/llm/inventory/immutable_value_spec.rb +132 -0
  42. data/spec/legion/extensions/llm/inventory/probe_coordinator_spec.rb +107 -0
  43. data/spec/legion/extensions/llm/inventory/probe_token_spec.rb +107 -0
  44. data/spec/legion/extensions/llm/inventory/publisher_spec.rb +104 -0
  45. data/spec/legion/extensions/llm/inventory/records_spec.rb +300 -0
  46. data/spec/legion/extensions/llm/inventory/registry_activation_spec.rb +140 -0
  47. data/spec/legion/extensions/llm/inventory/registry_availability_spec.rb +152 -0
  48. data/spec/legion/extensions/llm/inventory/registry_callable_spec.rb +63 -0
  49. data/spec/legion/extensions/llm/inventory/registry_replacement_spec.rb +69 -0
  50. data/spec/legion/extensions/llm/inventory/scoped_refresher_spec.rb +128 -0
  51. data/spec/legion/extensions/llm/inventory/snapshot_spec.rb +109 -0
  52. data/spec/legion/extensions/llm/provider_contract_spec.rb +20 -0
  53. data/spec/legion/extensions/llm/provider_spec.rb +83 -0
  54. data/spec/legion/extensions/llm/routing/provider_outcome_spec.rb +59 -0
  55. data/spec/legion/extensions/llm/routing/records_spec.rb +173 -0
  56. data/spec/legion/extensions/llm/taxonomies_spec.rb +51 -0
  57. data/spec/support/fake_llm_provider.rb +11 -0
  58. data/spec/support/fake_ssot_harness.rb +137 -0
  59. data/spec/support/ssot_registry_helpers.rb +75 -0
  60. metadata +36 -1
@@ -0,0 +1,592 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/llm/inventory/errors'
4
+ require 'legion/extensions/llm/inventory/immutable_value'
5
+ require 'legion/extensions/llm/inventory/identity'
6
+ require 'legion/extensions/llm/inventory/evidence'
7
+ require 'legion/extensions/llm/inventory/callable_handle'
8
+ require 'legion/extensions/llm/taxonomies'
9
+ require 'legion/extensions/llm/capabilities'
10
+
11
+ module Legion
12
+ module Extensions
13
+ module Llm
14
+ module Inventory
15
+ # Documentation namespace anchor for the immutable inventory records
16
+ # defined in this file: OfferingDraft, OfferingRecord, LaneRecord,
17
+ # AvailabilityFact, ReadinessResult, InstanceRecord, PublicationStatus,
18
+ # and MutationResult. The records themselves live directly under
19
+ # Inventory so their canonical constant paths stay flat.
20
+ module Records; end
21
+
22
+ # Shared normalization/validation used by the immutable inventory and
23
+ # routing records. Kept internal to the records so no record implements a
24
+ # competing validator. See phase-1-lex-llm-additive.md sections 10 and 15.
25
+ module RecordSupport
26
+ module_function
27
+
28
+ SECRET_KEY_TOKENS = %w[
29
+ credential authorization token secret password apikey prompt endpointurl callable
30
+ ].freeze
31
+
32
+ PUBLICATION_SOURCES = %i[provider_catalog provider_static_catalog provider_control_plane].freeze
33
+
34
+ def check_unknown_kwargs!(kwargs:, members:)
35
+ unknown = kwargs.keys - members
36
+ raise Errors::ValidationError, "unknown keyword(s): #{unknown.join(', ')}" unless unknown.empty?
37
+ end
38
+
39
+ def valid_utf8?(string)
40
+ [::Encoding::UTF_8, ::Encoding::US_ASCII].include?(string.encoding) && string.valid_encoding?
41
+ end
42
+
43
+ def sanitized_reason(value:, field:, max_bytes: 1024)
44
+ raise Errors::ValidationError, "#{field} must be a String" unless value.is_a?(::String)
45
+ raise Errors::ValidationError, "#{field} is not valid UTF-8" unless valid_utf8?(value)
46
+
47
+ trimmed = value.strip
48
+ raise Errors::ValidationError, "#{field} must not be empty" if trimmed.empty?
49
+ raise Errors::ValidationError, "#{field} exceeds #{max_bytes} UTF-8 bytes" if trimmed.bytesize > max_bytes
50
+
51
+ trimmed.b.dup.force_encoding(::Encoding::UTF_8).freeze
52
+ end
53
+
54
+ def optional_sanitized_reason(value:, field:, max_bytes: 1024)
55
+ return nil if value.nil?
56
+
57
+ sanitized_reason(value: value, field: field, max_bytes: max_bytes)
58
+ end
59
+
60
+ def nonnegative_integer(value:, field:)
61
+ raise Errors::ValidationError, "#{field} must be a nonnegative Integer" unless nonnegative_integer?(value)
62
+
63
+ value
64
+ end
65
+
66
+ def nonnegative_integer?(value)
67
+ value.is_a?(::Integer) && !value.negative?
68
+ end
69
+
70
+ def boolean!(value:, field:)
71
+ return value if [true, false].include?(value)
72
+
73
+ raise Errors::ValidationError, "#{field} must be true or false"
74
+ end
75
+
76
+ def normalize_provider_family(value:, field: :provider_family)
77
+ text = Identity.normalize_text(value: value, field: field).downcase
78
+ raise Errors::ValidationError, "#{field} must be snake-case ASCII" unless text.match?(/\A[a-z][a-z0-9_]*\z/)
79
+
80
+ text.to_sym
81
+ end
82
+
83
+ def optional_time(value:, field:)
84
+ return nil if value.nil?
85
+ raise Errors::ValidationError, "#{field} must be a Time" unless value.is_a?(::Time)
86
+
87
+ value.dup.freeze
88
+ end
89
+
90
+ def frozen_metadata(value:, field: :metadata)
91
+ reject_secret_keys!(value, field)
92
+ ImmutableValue.copy_and_freeze(value: value, field: field)
93
+ end
94
+
95
+ def reject_secret_keys!(value, field)
96
+ case value
97
+ when ::Hash
98
+ value.each do |key, nested|
99
+ raise Errors::ValidationError, "#{field} contains a secret-like key" if secret_key?(key)
100
+
101
+ reject_secret_keys!(nested, field)
102
+ end
103
+ when ::Array
104
+ value.each { |element| reject_secret_keys!(element, field) }
105
+ end
106
+ end
107
+
108
+ def secret_key?(key)
109
+ normalized = key.to_s.downcase.gsub(/[^a-z0-9]/, '')
110
+ SECRET_KEY_TOKENS.any? { |token| normalized.include?(token) }
111
+ end
112
+
113
+ def validate_operation_evidence!(evidence)
114
+ raise Errors::ValidationError, 'operation_evidence must be a Hash' unless evidence.is_a?(::Hash)
115
+
116
+ normalized = {}
117
+ evidence.each do |key, value|
118
+ op = Taxonomies.normalize_operation(value: key, allow_aliases: false)
119
+ raise Errors::ValidationError, "operation_evidence[#{op}] must be an OperationEvidence" unless value.is_a?(OperationEvidence)
120
+ raise Errors::ValidationError, "operation_evidence[#{op}] operation must match key" unless value.operation == op
121
+ raise Errors::ValidationError, "duplicate operation_evidence key #{op}" if normalized.key?(op)
122
+
123
+ normalized[op] = value
124
+ end
125
+ raise Errors::ValidationError, 'operation_evidence keys must equal Taxonomies::OPERATIONS exactly' unless normalized.keys.sort == Taxonomies::OPERATIONS.sort
126
+
127
+ normalized.freeze
128
+ end
129
+
130
+ def validate_capability_evidence!(evidence)
131
+ raise Errors::ValidationError, 'capability_evidence must be a Hash' unless evidence.is_a?(::Hash)
132
+
133
+ normalized = {}
134
+ evidence.each do |key, value|
135
+ cap = Legion::Extensions::Llm::Capabilities.canonical(key)
136
+ raise Errors::ValidationError, "capability_evidence key #{cap} is not canonical" unless Legion::Extensions::Llm::Capabilities::CANONICAL.include?(cap)
137
+ raise Errors::ValidationError, "capability_evidence[#{cap}] must be a CapabilityEvidence" unless value.is_a?(CapabilityEvidence)
138
+ raise Errors::ValidationError, "capability_evidence[#{cap}] capability must match key" unless value.capability == cap
139
+ raise Errors::ValidationError, "duplicate capability_evidence key #{cap}" if normalized.key?(cap)
140
+
141
+ normalized[cap] = value
142
+ end
143
+ normalized.freeze
144
+ end
145
+
146
+ def validate_quota_domains!(quota_domains)
147
+ raise Errors::ValidationError, 'quota_domains must be a Hash' unless quota_domains.is_a?(::Hash)
148
+
149
+ normalized = {}
150
+ quota_domains.each do |key, value|
151
+ op = Taxonomies.normalize_operation(value: key, allow_aliases: false)
152
+ normalized[op] = validate_quota_domain_value!(value: value, field: "quota_domains[#{op}]")
153
+ end
154
+ normalized.freeze
155
+ end
156
+
157
+ def validate_quota_domain_value!(value:, field:)
158
+ raise Errors::ValidationError, "#{field} must be a nonempty opaque String" unless value.is_a?(::String) && valid_utf8?(value) && !value.strip.empty?
159
+
160
+ value.dup.freeze
161
+ end
162
+
163
+ # Validates and returns the five scalar ValueEvidence fields shared by
164
+ # OfferingDraft, OfferingRecord, and LaneRecord as a member Hash.
165
+ def scalar_value_evidences(kwargs)
166
+ {
167
+ context_evidence: positive_int_value_evidence!(kwargs[:context_evidence], :context_evidence),
168
+ max_output_evidence: positive_int_value_evidence!(kwargs[:max_output_evidence], :max_output_evidence),
169
+ embedding_dimensions_evidence: embedding_dimensions_value_evidence!(
170
+ kwargs[:embedding_dimensions_evidence], :embedding_dimensions_evidence
171
+ ),
172
+ model_revision_evidence: model_revision_value_evidence!(
173
+ kwargs[:model_revision_evidence], :model_revision_evidence
174
+ ),
175
+ tokenizer_evidence: tokenizer_value_evidence!(kwargs[:tokenizer_evidence], :tokenizer_evidence)
176
+ }
177
+ end
178
+
179
+ def value_evidence!(evidence, field)
180
+ raise Errors::ValidationError, "#{field} must be a ValueEvidence" unless evidence.is_a?(ValueEvidence)
181
+
182
+ evidence
183
+ end
184
+
185
+ def positive_int_value_evidence!(evidence, field)
186
+ value_evidence!(evidence, field)
187
+ raise Errors::ValidationError, "known #{field} must be a positive Integer" if evidence.known? && !(evidence.value.is_a?(::Integer) && evidence.value.positive?)
188
+
189
+ evidence
190
+ end
191
+
192
+ def embedding_dimensions_value_evidence!(evidence, field)
193
+ value_evidence!(evidence, field)
194
+ return evidence unless evidence.known?
195
+
196
+ dims = evidence.value
197
+ valid = dims.is_a?(::Array) && !dims.empty? &&
198
+ dims.all? { |dim| dim.is_a?(::Integer) && dim.positive? } &&
199
+ dims == dims.uniq && dims == dims.sort
200
+ raise Errors::ValidationError, "known #{field} must be a sorted, unique Array of positive Integers" unless valid
201
+
202
+ evidence
203
+ end
204
+
205
+ def model_revision_value_evidence!(evidence, field)
206
+ value_evidence!(evidence, field)
207
+ return evidence unless evidence.known?
208
+
209
+ revision = evidence.value
210
+ valid = revision.is_a?(::String) && !revision.strip.empty? &&
211
+ valid_utf8?(revision) && revision.unicode_normalized?(:nfc)
212
+ raise Errors::ValidationError, "known #{field} must be a nonempty NFC String" unless valid
213
+
214
+ evidence
215
+ end
216
+
217
+ def tokenizer_value_evidence!(evidence, field)
218
+ value_evidence!(evidence, field)
219
+ return evidence unless evidence.known?
220
+
221
+ descriptor = evidence.value
222
+ unless descriptor.is_a?(::Hash) && descriptor.keys.sort == %i[estimator parameters version]
223
+ raise Errors::ValidationError, "known #{field} must be exactly {estimator:, version:, parameters:}"
224
+ end
225
+
226
+ nonempty_nfc!(value: descriptor[:estimator], field: "#{field}.estimator")
227
+ nonempty_nfc!(value: descriptor[:version], field: "#{field}.version")
228
+ raise Errors::ValidationError, "#{field}.parameters must be a Hash" unless descriptor[:parameters].is_a?(::Hash)
229
+
230
+ evidence
231
+ end
232
+
233
+ def nonempty_nfc!(value:, field:)
234
+ valid = value.is_a?(::String) && !value.strip.empty? && valid_utf8?(value) && value.unicode_normalized?(:nfc)
235
+ raise Errors::ValidationError, "#{field} must be a nonempty NFC String" unless valid
236
+
237
+ value
238
+ end
239
+
240
+ def publication_source!(value:)
241
+ raise Errors::ValidationError, "publication_source must be one of #{PUBLICATION_SOURCES.inspect}" unless PUBLICATION_SOURCES.include?(value)
242
+
243
+ value
244
+ end
245
+ end
246
+
247
+ # An off-registry provider draft of one offering. Carries no provider
248
+ # family, instance ID, offering ID, lane ID, callable, health, weight, or
249
+ # default model. See section 10.1.
250
+ OfferingDraft = ::Data.define(
251
+ :provider_native_key, :model, :tier, :operation_evidence, :capability_evidence,
252
+ :context_evidence, :max_output_evidence, :embedding_dimensions_evidence,
253
+ :model_revision_evidence, :tokenizer_evidence, :quota_domains, :metadata, :publication_source
254
+ ) do
255
+ def initialize(**kwargs)
256
+ kwargs = { capability_evidence: {}, quota_domains: {}, metadata: {} }.merge(kwargs)
257
+ RecordSupport.check_unknown_kwargs!(kwargs: kwargs, members: self.class.members)
258
+
259
+ super(
260
+ provider_native_key: Identity.normalize_text(value: kwargs[:provider_native_key], field: :provider_native_key),
261
+ model: Identity.normalize_text(value: kwargs[:model], field: :model),
262
+ tier: Identity.normalize_enum(value: kwargs[:tier], field: :tier, allowed: Taxonomies::TIERS),
263
+ operation_evidence: RecordSupport.validate_operation_evidence!(kwargs[:operation_evidence]),
264
+ capability_evidence: RecordSupport.validate_capability_evidence!(kwargs[:capability_evidence]),
265
+ quota_domains: RecordSupport.validate_quota_domains!(kwargs[:quota_domains]),
266
+ metadata: RecordSupport.frozen_metadata(value: kwargs[:metadata]),
267
+ publication_source: RecordSupport.publication_source!(value: kwargs[:publication_source]),
268
+ **RecordSupport.scalar_value_evidences(kwargs)
269
+ )
270
+ end
271
+ end
272
+
273
+ # A registry-owned offering with canonical identity and captured callable.
274
+ # Only Inventory::Publisher/Registry constructs it. See section 10.2.
275
+ OfferingRecord = ::Data.define(
276
+ :offering_id, :provider_native_key, :instance_key, :model, :tier, :operation_evidence,
277
+ :capability_evidence, :context_evidence, :max_output_evidence, :embedding_dimensions_evidence,
278
+ :model_revision_evidence, :tokenizer_evidence, :quota_domains, :metadata, :callable_handle,
279
+ :publication_source
280
+ ) do
281
+ def initialize(**kwargs)
282
+ RecordSupport.check_unknown_kwargs!(kwargs: kwargs, members: self.class.members)
283
+ instance_key = kwargs[:instance_key]
284
+ callable_handle = kwargs[:callable_handle]
285
+ raise Errors::ValidationError, 'instance_key must be an InstanceKey' unless instance_key.is_a?(Identity::InstanceKey)
286
+ raise Errors::ValidationError, 'callable_handle must be a CallableHandle' unless callable_handle.is_a?(CallableHandle)
287
+
288
+ native_key = Identity.normalize_text(value: kwargs[:provider_native_key], field: :provider_native_key)
289
+ Identity.validate_offering_id!(value: kwargs[:offering_id], instance_key: instance_key, provider_native_key: native_key)
290
+
291
+ super(
292
+ offering_id: kwargs[:offering_id].dup.freeze,
293
+ provider_native_key: native_key,
294
+ instance_key: instance_key,
295
+ model: Identity.normalize_text(value: kwargs[:model], field: :model),
296
+ tier: Identity.normalize_enum(value: kwargs[:tier], field: :tier, allowed: Taxonomies::TIERS),
297
+ operation_evidence: RecordSupport.validate_operation_evidence!(kwargs[:operation_evidence]),
298
+ capability_evidence: RecordSupport.validate_capability_evidence!(kwargs[:capability_evidence]),
299
+ quota_domains: RecordSupport.validate_quota_domains!(kwargs[:quota_domains]),
300
+ metadata: RecordSupport.frozen_metadata(value: kwargs[:metadata]),
301
+ callable_handle: callable_handle,
302
+ publication_source: RecordSupport.publication_source!(value: kwargs[:publication_source]),
303
+ **RecordSupport.scalar_value_evidences(kwargs)
304
+ )
305
+ end
306
+
307
+ def supported_operations
308
+ operation_evidence.select { |_op, evidence| evidence.supported? }.keys.sort
309
+ end
310
+
311
+ def unsupported_operations
312
+ operation_evidence.select { |_op, evidence| evidence.unsupported? }.keys.sort
313
+ end
314
+
315
+ def unknown_operations
316
+ operation_evidence.select { |_op, evidence| evidence.unknown? }.keys.sort
317
+ end
318
+
319
+ def operation_status(operation:)
320
+ operation_evidence.fetch(Taxonomies.normalize_operation(value: operation, allow_aliases: false)).status
321
+ end
322
+
323
+ def capability_evidence_for(capability:)
324
+ capability_evidence[Legion::Extensions::Llm::Capabilities.canonical(capability)]
325
+ end
326
+
327
+ def capability_status(capability:)
328
+ evidence = capability_evidence_for(capability: capability)
329
+ evidence.nil? ? :unknown : evidence.status
330
+ end
331
+
332
+ def quota_domain(operation:)
333
+ quota_domains[Taxonomies.normalize_operation(value: operation, allow_aliases: false)]
334
+ end
335
+ end
336
+
337
+ # An executable lane derived by the registry from one supported operation
338
+ # of an OfferingRecord. See section 10.3.
339
+ LaneRecord = ::Data.define(
340
+ :lane_id, :offering_id, :instance_key, :provider_family, :instance_id, :model, :tier, :operation,
341
+ :capability_evidence, :context_evidence, :max_output_evidence, :embedding_dimensions_evidence,
342
+ :model_revision_evidence, :tokenizer_evidence, :quota_domain, :metadata, :callable_handle,
343
+ :publication_source
344
+ ) do
345
+ def initialize(**kwargs)
346
+ RecordSupport.check_unknown_kwargs!(kwargs: kwargs, members: self.class.members)
347
+ instance_key = kwargs[:instance_key]
348
+ callable_handle = kwargs[:callable_handle]
349
+ raise Errors::ValidationError, 'instance_key must be an InstanceKey' unless instance_key.is_a?(Identity::InstanceKey)
350
+ raise Errors::ValidationError, 'callable_handle must be a CallableHandle' unless callable_handle.is_a?(CallableHandle)
351
+
352
+ family = RecordSupport.normalize_provider_family(value: kwargs[:provider_family])
353
+ resolved_instance = Identity.normalize_text(value: kwargs[:instance_id], field: :instance_id)
354
+ unless family == instance_key.provider_family && resolved_instance == instance_key.instance_id
355
+ raise Errors::ValidationError, 'provider_family and instance_id must equal instance_key'
356
+ end
357
+
358
+ canonical_model = Identity.normalize_text(value: kwargs[:model], field: :model)
359
+ canonical_operation = Taxonomies.normalize_operation(value: kwargs[:operation], allow_aliases: false)
360
+ Identity.validate_lane_id!(
361
+ value: kwargs[:lane_id], instance_key: instance_key, operation: canonical_operation,
362
+ model: canonical_model, offering_id: kwargs[:offering_id]
363
+ )
364
+
365
+ super(
366
+ lane_id: kwargs[:lane_id].dup.freeze,
367
+ offering_id: kwargs[:offering_id].dup.freeze,
368
+ instance_key: instance_key,
369
+ provider_family: family,
370
+ instance_id: resolved_instance,
371
+ model: canonical_model,
372
+ tier: Identity.normalize_enum(value: kwargs[:tier], field: :tier, allowed: Taxonomies::TIERS),
373
+ operation: canonical_operation,
374
+ capability_evidence: RecordSupport.validate_capability_evidence!(kwargs[:capability_evidence]),
375
+ quota_domain: kwargs[:quota_domain].nil? ? nil : RecordSupport.validate_quota_domain_value!(value: kwargs[:quota_domain], field: :quota_domain),
376
+ metadata: RecordSupport.frozen_metadata(value: kwargs[:metadata]),
377
+ callable_handle: callable_handle,
378
+ publication_source: RecordSupport.publication_source!(value: kwargs[:publication_source]),
379
+ **RecordSupport.scalar_value_evidences(kwargs)
380
+ )
381
+ end
382
+ end
383
+
384
+ # The availability state of an exact instance. Carries no cooldown,
385
+ # half-open, latency, success-count, or clock-expiry field. See 10.4.
386
+ AvailabilityFact = ::Data.define(
387
+ :state, :availability_revision, :source, :reason, :observed_at,
388
+ :last_probe_started_at, :last_probe_completed_at, :last_probe_outcome, :unavailable_revision
389
+ ) do
390
+ def initialize(**kwargs)
391
+ kwargs = {
392
+ last_probe_started_at: nil, last_probe_completed_at: nil, last_probe_outcome: nil, unavailable_revision: nil
393
+ }.merge(kwargs)
394
+ RecordSupport.check_unknown_kwargs!(kwargs: kwargs, members: self.class.members)
395
+
396
+ state = kwargs[:state]
397
+ availability_revision = kwargs[:availability_revision]
398
+ unavailable_revision = kwargs[:unavailable_revision]
399
+ raise Errors::ValidationError, 'state must be a Taxonomies::AVAILABILITY_STATES value' unless Taxonomies::AVAILABILITY_STATES.include?(state)
400
+ raise Errors::ValidationError, 'source must be a Taxonomies::AVAILABILITY_SOURCES value' unless Taxonomies::AVAILABILITY_SOURCES.include?(kwargs[:source])
401
+
402
+ RecordSupport.nonnegative_integer(value: availability_revision, field: :availability_revision)
403
+ validate_unavailable_revision!(state, availability_revision, unavailable_revision)
404
+ resolved_outcome = validate_probe_telemetry!(
405
+ kwargs[:last_probe_started_at], kwargs[:last_probe_completed_at], kwargs[:last_probe_outcome]
406
+ )
407
+
408
+ super(
409
+ state: state,
410
+ availability_revision: availability_revision,
411
+ source: kwargs[:source],
412
+ reason: RecordSupport.sanitized_reason(value: kwargs[:reason], field: :reason),
413
+ observed_at: RecordSupport.optional_time(value: kwargs[:observed_at], field: :observed_at),
414
+ last_probe_started_at: RecordSupport.optional_time(value: kwargs[:last_probe_started_at], field: :last_probe_started_at),
415
+ last_probe_completed_at: RecordSupport.optional_time(value: kwargs[:last_probe_completed_at], field: :last_probe_completed_at),
416
+ last_probe_outcome: resolved_outcome,
417
+ unavailable_revision: unavailable_revision
418
+ )
419
+ end
420
+
421
+ def validate_unavailable_revision!(state, availability_revision, unavailable_revision)
422
+ if state == :unavailable
423
+ raise Errors::ValidationError, 'unavailable state requires unavailable_revision == availability_revision' unless unavailable_revision == availability_revision
424
+ elsif !unavailable_revision.nil?
425
+ raise Errors::ValidationError, 'non-unavailable state requires unavailable_revision: nil'
426
+ end
427
+ end
428
+
429
+ def validate_probe_telemetry!(started_at, completed_at, outcome)
430
+ return nil if outcome.nil?
431
+ raise Errors::ValidationError, 'last_probe_outcome must be nil, :success, or :failure' unless Taxonomies::PROBE_OUTCOMES.include?(outcome)
432
+ raise Errors::ValidationError, 'a non-nil last_probe_outcome requires both probe timestamps' unless started_at.is_a?(::Time) && completed_at.is_a?(::Time)
433
+ raise Errors::ValidationError, 'probe completion cannot precede start' if completed_at < started_at
434
+
435
+ outcome
436
+ end
437
+ end
438
+
439
+ # The result of a provider-specific safe readiness operation. Contains no
440
+ # model output, prompt, tokens, callable, exception, or transition
441
+ # instruction. See section 10.5.
442
+ ReadinessResult = ::Data.define(:ready, :reason, :metadata) do
443
+ def initialize(ready:, reason:, metadata: {})
444
+ super(
445
+ ready: RecordSupport.boolean!(value: ready, field: :ready),
446
+ reason: RecordSupport.sanitized_reason(value: reason, field: :reason),
447
+ metadata: RecordSupport.frozen_metadata(value: metadata)
448
+ )
449
+ end
450
+
451
+ def ready?
452
+ ready
453
+ end
454
+ end
455
+
456
+ # An activated exact instance's complete published state. Its availability
457
+ # is available or unavailable, never initializing. See section 10.6.
458
+ InstanceRecord = ::Data.define(
459
+ :instance_key, :callable_handle, :availability, :offerings_by_id,
460
+ :publisher_id, :publisher_token_id, :published_sequence, :published_at
461
+ ) do
462
+ def initialize(**kwargs)
463
+ RecordSupport.check_unknown_kwargs!(kwargs: kwargs, members: self.class.members)
464
+ instance_key = kwargs[:instance_key]
465
+ callable_handle = kwargs[:callable_handle]
466
+ availability = kwargs[:availability]
467
+ raise Errors::ValidationError, 'instance_key must be an InstanceKey' unless instance_key.is_a?(Identity::InstanceKey)
468
+ raise Errors::ValidationError, 'callable_handle must be a CallableHandle' unless callable_handle.is_a?(CallableHandle)
469
+ unless availability.is_a?(AvailabilityFact) && %i[available unavailable].include?(availability.state)
470
+ raise Errors::ValidationError, 'availability must be an available/unavailable AvailabilityFact'
471
+ end
472
+
473
+ super(
474
+ instance_key: instance_key,
475
+ callable_handle: callable_handle,
476
+ availability: availability,
477
+ offerings_by_id: validate_offerings!(kwargs[:offerings_by_id], instance_key, callable_handle),
478
+ publisher_id: nonempty_id!(kwargs[:publisher_id], :publisher_id),
479
+ publisher_token_id: nonempty_id!(kwargs[:publisher_token_id], :publisher_token_id),
480
+ published_sequence: RecordSupport.nonnegative_integer(value: kwargs[:published_sequence], field: :published_sequence),
481
+ published_at: require_time!(kwargs[:published_at], :published_at)
482
+ )
483
+ end
484
+
485
+ def validate_offerings!(offerings_by_id, instance_key, callable_handle)
486
+ raise Errors::ValidationError, 'offerings_by_id must be a Hash' unless offerings_by_id.is_a?(::Hash)
487
+
488
+ offerings_by_id.each do |offering_id, offering|
489
+ raise Errors::ValidationError, "offering #{offering_id} must be an OfferingRecord" unless offering.is_a?(OfferingRecord)
490
+ raise Errors::ValidationError, "offering #{offering_id} key mismatch" unless offering.offering_id == offering_id
491
+ unless offering.instance_key == instance_key && offering.callable_handle.equal?(callable_handle)
492
+ raise Errors::ValidationError, "offering #{offering_id} must share the instance callable/key"
493
+ end
494
+ end
495
+ offerings_by_id.dup.freeze
496
+ end
497
+
498
+ def nonempty_id!(value, field)
499
+ raise Errors::ValidationError, "#{field} must be a nonempty String" unless value.is_a?(::String) && !value.empty?
500
+
501
+ value.dup.freeze
502
+ end
503
+
504
+ def require_time!(value, field)
505
+ raise Errors::ValidationError, "#{field} must be a Time" unless value.is_a?(::Time)
506
+
507
+ value.dup.freeze
508
+ end
509
+ end
510
+
511
+ # Publication-scope status distinguishing an initializing claim from an
512
+ # authoritative complete catalog. See section 10.7.
513
+ PublicationStatus = ::Data.define(
514
+ :instance_key, :state, :publisher_token_id, :published_sequence,
515
+ :last_probe_started_at, :last_probe_completed_at, :last_probe_outcome, :last_error
516
+ ) do
517
+ def initialize(**kwargs)
518
+ kwargs = {
519
+ last_probe_started_at: nil, last_probe_completed_at: nil, last_probe_outcome: nil, last_error: nil
520
+ }.merge(kwargs)
521
+ RecordSupport.check_unknown_kwargs!(kwargs: kwargs, members: self.class.members)
522
+
523
+ instance_key = kwargs[:instance_key]
524
+ published_sequence = kwargs[:published_sequence]
525
+ last_probe_outcome = kwargs[:last_probe_outcome]
526
+ raise Errors::ValidationError, 'instance_key must be an InstanceKey' unless instance_key.is_a?(Identity::InstanceKey)
527
+ raise Errors::ValidationError, 'state must be :initializing or :complete' unless Taxonomies::PUBLICATION_STATES.include?(kwargs[:state])
528
+ raise Errors::ValidationError, 'published_sequence must be nil or a nonnegative Integer' unless published_sequence.nil? || RecordSupport.nonnegative_integer?(published_sequence)
529
+ raise Errors::ValidationError, 'last_probe_outcome must be nil, :success, or :failure' unless last_probe_outcome.nil? || Taxonomies::PROBE_OUTCOMES.include?(last_probe_outcome)
530
+
531
+ super(
532
+ instance_key: instance_key,
533
+ state: kwargs[:state],
534
+ publisher_token_id: kwargs[:publisher_token_id].nil? ? nil : kwargs[:publisher_token_id].dup.freeze,
535
+ published_sequence: published_sequence,
536
+ last_probe_started_at: RecordSupport.optional_time(value: kwargs[:last_probe_started_at], field: :last_probe_started_at),
537
+ last_probe_completed_at: RecordSupport.optional_time(value: kwargs[:last_probe_completed_at], field: :last_probe_completed_at),
538
+ last_probe_outcome: last_probe_outcome,
539
+ last_error: RecordSupport.optional_sanitized_reason(value: kwargs[:last_error], field: :last_error)
540
+ )
541
+ end
542
+ end
543
+
544
+ # The outcome of a registry mutation. Expected stale results use
545
+ # applied: false; every other reason requires applied: true. See 10.8.
546
+ MutationResult = ::Data.define(
547
+ :applied, :reason, :generation, :instance_key, :publication_status, :instance_record
548
+ ) do
549
+ def initialize(**kwargs)
550
+ kwargs = { publication_status: nil, instance_record: nil }.merge(kwargs)
551
+ RecordSupport.check_unknown_kwargs!(kwargs: kwargs, members: self.class.members)
552
+
553
+ applied = kwargs[:applied]
554
+ reason = kwargs[:reason]
555
+ instance_key = kwargs[:instance_key]
556
+ publication_status = kwargs[:publication_status]
557
+ instance_record = kwargs[:instance_record]
558
+ RecordSupport.boolean!(value: applied, field: :applied)
559
+ raise Errors::ValidationError, 'reason must be a Taxonomies::MUTATION_REASONS value' unless Taxonomies::MUTATION_REASONS.include?(reason)
560
+
561
+ validate_applied_reason!(applied, reason)
562
+ raise Errors::ValidationError, 'instance_key must be an InstanceKey' unless instance_key.is_a?(Identity::InstanceKey)
563
+ raise Errors::ValidationError, 'publication_status must be a PublicationStatus or nil' unless publication_status.nil? || publication_status.is_a?(PublicationStatus)
564
+ raise Errors::ValidationError, 'instance_record must be an InstanceRecord or nil' unless instance_record.nil? || instance_record.is_a?(InstanceRecord)
565
+
566
+ super(
567
+ applied: applied,
568
+ reason: reason,
569
+ generation: RecordSupport.nonnegative_integer(value: kwargs[:generation], field: :generation),
570
+ instance_key: instance_key,
571
+ publication_status: publication_status,
572
+ instance_record: instance_record
573
+ )
574
+ end
575
+
576
+ def applied?
577
+ applied
578
+ end
579
+
580
+ def validate_applied_reason!(applied, reason)
581
+ non_applied = %i[stale_publisher stale_probe already_removed]
582
+ if applied
583
+ raise Errors::ValidationError, "applied result cannot use reason #{reason}" if non_applied.include?(reason)
584
+ elsif !non_applied.include?(reason)
585
+ raise Errors::ValidationError, "non-applied result requires a stale/already-removed reason, got #{reason}"
586
+ end
587
+ end
588
+ end
589
+ end
590
+ end
591
+ end
592
+ end