lex-llm 0.7.6 → 0.8.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 (162) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +87 -0
  3. data/RULES.md +97 -0
  4. data/lib/legion/extensions/llm/auto_registration.rb +4 -13
  5. data/lib/legion/extensions/llm/canonical/chunk.rb +106 -92
  6. data/lib/legion/extensions/llm/canonical/content_block.rb +66 -75
  7. data/lib/legion/extensions/llm/canonical/message.rb +52 -67
  8. data/lib/legion/extensions/llm/canonical/params.rb +59 -33
  9. data/lib/legion/extensions/llm/canonical/request.rb +67 -54
  10. data/lib/legion/extensions/llm/canonical/response.rb +61 -76
  11. data/lib/legion/extensions/llm/canonical/strict.rb +105 -0
  12. data/lib/legion/extensions/llm/canonical/thinking.rb +99 -51
  13. data/lib/legion/extensions/llm/canonical/tool_call.rb +42 -51
  14. data/lib/legion/extensions/llm/canonical/tool_definition.rb +56 -46
  15. data/lib/legion/extensions/llm/canonical/tool_schema.rb +15 -22
  16. data/lib/legion/extensions/llm/canonical/usage.rb +47 -40
  17. data/lib/legion/extensions/llm/canonical.rb +6 -5
  18. data/lib/legion/extensions/llm/configuration.rb +40 -10
  19. data/lib/legion/extensions/llm/connection.rb +8 -30
  20. data/lib/legion/extensions/llm/credential_sources.rb +32 -49
  21. data/lib/legion/extensions/llm/discovery/actor.rb +92 -0
  22. data/lib/legion/extensions/llm/discovery/pipeline.rb +604 -0
  23. data/lib/legion/extensions/llm/error.rb +0 -14
  24. data/lib/legion/extensions/llm/fleet/contract_error.rb +15 -0
  25. data/lib/legion/extensions/llm/fleet/envelope_validation.rb +7 -6
  26. data/lib/legion/extensions/llm/fleet/fleet_envelope.rb +66 -0
  27. data/lib/legion/extensions/llm/fleet/protocol.rb +18 -5
  28. data/lib/legion/extensions/llm/fleet/provider_responder.rb +58 -156
  29. data/lib/legion/extensions/llm/fleet/token_validator.rb +15 -21
  30. data/lib/legion/extensions/llm/fleet/worker_execution.rb +107 -154
  31. data/lib/legion/extensions/llm/inventory/errors.rb +0 -1
  32. data/lib/legion/extensions/llm/inventory/evidence.rb +1 -1
  33. data/lib/legion/extensions/llm/inventory/identity.rb +55 -39
  34. data/lib/legion/extensions/llm/inventory/probe_token.rb +9 -12
  35. data/lib/legion/extensions/llm/inventory/publisher.rb +15 -53
  36. data/lib/legion/extensions/llm/inventory/records.rb +36 -100
  37. data/lib/legion/extensions/llm/inventory/registry.rb +54 -48
  38. data/lib/legion/extensions/llm/inventory/snapshot.rb +7 -21
  39. data/lib/legion/extensions/llm/inventory/weight_reconciler.rb +13 -6
  40. data/lib/legion/extensions/llm/inventory/weight_schema.rb +47 -8
  41. data/lib/legion/extensions/llm/provider/open_ai_compatible.rb +172 -182
  42. data/lib/legion/extensions/llm/provider.rb +192 -315
  43. data/lib/legion/extensions/llm/provider_contract.rb +25 -8
  44. data/lib/legion/extensions/llm/provider_settings.rb +5 -26
  45. data/lib/legion/extensions/llm/responses/thinking_extractor.rb +8 -1
  46. data/lib/legion/extensions/llm/responses/tool_arguments.rb +34 -0
  47. data/lib/legion/extensions/llm/routing/provider_outcome.rb +25 -0
  48. data/lib/legion/extensions/llm/routing/records.rb +34 -17
  49. data/lib/legion/extensions/llm/stream_accumulator.rb +186 -270
  50. data/lib/legion/extensions/llm/streaming.rb +50 -35
  51. data/lib/legion/extensions/llm/taxonomies.rb +14 -26
  52. data/lib/legion/extensions/llm/transport/fleet_lane.rb +8 -10
  53. data/lib/legion/extensions/llm/transport/messages/fleet_error.rb +3 -2
  54. data/lib/legion/extensions/llm/transport/messages/fleet_request.rb +6 -8
  55. data/lib/legion/extensions/llm/transport/messages/fleet_response.rb +10 -9
  56. data/lib/legion/extensions/llm/utils.rb +23 -5
  57. data/lib/legion/extensions/llm/version.rb +1 -1
  58. data/lib/legion/extensions/llm.rb +8 -98
  59. data/spec/legion/extensions/llm/auto_registration_spec.rb +4 -9
  60. data/spec/legion/extensions/llm/canonical/chunk_spec.rb +66 -252
  61. data/spec/legion/extensions/llm/canonical/content_block_spec.rb +52 -197
  62. data/spec/legion/extensions/llm/canonical/message_spec.rb +89 -204
  63. data/spec/legion/extensions/llm/canonical/params_spec.rb +57 -136
  64. data/spec/legion/extensions/llm/canonical/request_spec.rb +81 -143
  65. data/spec/legion/extensions/llm/canonical/response_spec.rb +68 -204
  66. data/spec/legion/extensions/llm/canonical/thinking_spec.rb +68 -148
  67. data/spec/legion/extensions/llm/canonical/tool_call_spec.rb +59 -162
  68. data/spec/legion/extensions/llm/canonical/tool_definition_spec.rb +55 -191
  69. data/spec/legion/extensions/llm/canonical/tool_schema_spec.rb +26 -67
  70. data/spec/legion/extensions/llm/canonical/usage_spec.rb +46 -155
  71. data/spec/legion/extensions/llm/configuration_spec.rb +31 -5
  72. data/spec/legion/extensions/llm/conformance/canonical_type_examples.rb +106 -0
  73. data/spec/legion/extensions/llm/conformance/conformance.rb +10 -2
  74. data/spec/legion/extensions/llm/conformance/provider_translator_examples.rb +1 -1
  75. data/spec/legion/extensions/llm/conformance/ssot_contract_conformance_spec.rb +130 -0
  76. data/spec/legion/extensions/llm/conformance/ssot_contract_examples.rb +507 -0
  77. data/spec/legion/extensions/llm/conformance/ssot_provider_examples.rb +11 -10
  78. data/spec/legion/extensions/llm/credential_sources_spec.rb +12 -13
  79. data/spec/legion/extensions/llm/error_spec.rb +2 -12
  80. data/spec/legion/extensions/llm/fleet/exact_offering_spec.rb +59 -45
  81. data/spec/legion/extensions/llm/fleet/provider_responder_spec.rb +173 -95
  82. data/spec/legion/extensions/llm/fleet/token_validator_spec.rb +7 -2
  83. data/spec/legion/extensions/llm/fleet/worker_execution_spec.rb +93 -74
  84. data/spec/legion/extensions/llm/fleet_messages_spec.rb +118 -123
  85. data/spec/legion/extensions/llm/inventory/boot_spec.rb +4 -4
  86. data/spec/legion/extensions/llm/inventory/identity_spec.rb +127 -110
  87. data/spec/legion/extensions/llm/inventory/probe_token_spec.rb +4 -4
  88. data/spec/legion/extensions/llm/inventory/publisher_spec.rb +8 -61
  89. data/spec/legion/extensions/llm/inventory/records_spec.rb +82 -47
  90. data/spec/legion/extensions/llm/inventory/registry_activation_spec.rb +49 -16
  91. data/spec/legion/extensions/llm/inventory/registry_replacement_spec.rb +5 -4
  92. data/spec/legion/extensions/llm/inventory/snapshot_spec.rb +11 -5
  93. data/spec/legion/extensions/llm/inventory/weight_reconciler_spec.rb +7 -3
  94. data/spec/legion/extensions/llm/inventory/weight_schema_spec.rb +60 -10
  95. data/spec/legion/extensions/llm/provider/open_ai_compatible_spec.rb +105 -68
  96. data/spec/legion/extensions/llm/provider/open_ai_compatible_tool_calls_array_spec.rb +7 -31
  97. data/spec/legion/extensions/llm/provider_contract_spec.rb +10 -15
  98. data/spec/legion/extensions/llm/provider_spec.rb +98 -78
  99. data/spec/legion/extensions/llm/routing/records_spec.rb +38 -5
  100. data/spec/legion/extensions/llm/stream_accumulator_spec.rb +174 -144
  101. data/spec/legion/extensions/llm/streaming_spec.rb +27 -0
  102. data/spec/legion/extensions/llm/taxonomies_spec.rb +43 -43
  103. data/spec/legion/extensions/llm/transport/fleet_lane_spec.rb +1 -1
  104. data/spec/legion/extensions/llm/utils_spec.rb +26 -7
  105. data/spec/legion/extensions/llm_base_contract_spec.rb +55 -90
  106. data/spec/legion/extensions/llm_extension_spec.rb +5 -5
  107. data/spec/support/fake_llm_provider.rb +45 -39
  108. data/spec/support/fake_ssot_harness.rb +7 -2
  109. metadata +11 -54
  110. data/lib/legion/extensions/llm/agent.rb +0 -366
  111. data/lib/legion/extensions/llm/aliases.json +0 -436
  112. data/lib/legion/extensions/llm/aliases.rb +0 -67
  113. data/lib/legion/extensions/llm/attachment.rb +0 -229
  114. data/lib/legion/extensions/llm/chat.rb +0 -354
  115. data/lib/legion/extensions/llm/chunk.rb +0 -10
  116. data/lib/legion/extensions/llm/content.rb +0 -81
  117. data/lib/legion/extensions/llm/context.rb +0 -33
  118. data/lib/legion/extensions/llm/embedding.rb +0 -33
  119. data/lib/legion/extensions/llm/image.rb +0 -109
  120. data/lib/legion/extensions/llm/inventory/capabilities.rb +0 -40
  121. data/lib/legion/extensions/llm/inventory/scoped_refresher.rb +0 -310
  122. data/lib/legion/extensions/llm/message.rb +0 -118
  123. data/lib/legion/extensions/llm/mime_type.rb +0 -75
  124. data/lib/legion/extensions/llm/model/info.rb +0 -286
  125. data/lib/legion/extensions/llm/model/modalities.rb +0 -26
  126. data/lib/legion/extensions/llm/model/pricing.rb +0 -52
  127. data/lib/legion/extensions/llm/model/pricing_category.rb +0 -50
  128. data/lib/legion/extensions/llm/model/pricing_tier.rb +0 -37
  129. data/lib/legion/extensions/llm/model.rb +0 -11
  130. data/lib/legion/extensions/llm/models.json +0 -57313
  131. data/lib/legion/extensions/llm/models.rb +0 -530
  132. data/lib/legion/extensions/llm/models_schema.json +0 -168
  133. data/lib/legion/extensions/llm/moderation.rb +0 -60
  134. data/lib/legion/extensions/llm/registry_event_builder.rb +0 -141
  135. data/lib/legion/extensions/llm/registry_publisher.rb +0 -107
  136. data/lib/legion/extensions/llm/responses/chat_response.rb +0 -43
  137. data/lib/legion/extensions/llm/responses/embedding_response.rb +0 -38
  138. data/lib/legion/extensions/llm/responses/stream_chunk.rb +0 -43
  139. data/lib/legion/extensions/llm/routing/lane_key.rb +0 -66
  140. data/lib/legion/extensions/llm/routing/model_offering.rb +0 -241
  141. data/lib/legion/extensions/llm/routing/offering_registry.rb +0 -101
  142. data/lib/legion/extensions/llm/routing/registry_event.rb +0 -167
  143. data/lib/legion/extensions/llm/thinking.rb +0 -53
  144. data/lib/legion/extensions/llm/tokens.rb +0 -51
  145. data/lib/legion/extensions/llm/tool_call.rb +0 -34
  146. data/lib/legion/extensions/llm/transcription.rb +0 -39
  147. data/lib/legion/extensions/llm/transport/messages/registry_event.rb +0 -44
  148. data/spec/legion/extensions/llm/agent_spec.rb +0 -179
  149. data/spec/legion/extensions/llm/attachment_spec.rb +0 -25
  150. data/spec/legion/extensions/llm/conformance/fixtures/ssot_identity_vectors.json +0 -84
  151. data/spec/legion/extensions/llm/context_spec.rb +0 -127
  152. data/spec/legion/extensions/llm/inventory/capabilities_spec.rb +0 -43
  153. data/spec/legion/extensions/llm/inventory/scoped_refresher_spec.rb +0 -340
  154. data/spec/legion/extensions/llm/message_spec.rb +0 -64
  155. data/spec/legion/extensions/llm/model/info_spec.rb +0 -222
  156. data/spec/legion/extensions/llm/models_spec.rb +0 -104
  157. data/spec/legion/extensions/llm/registry_event_builder_spec.rb +0 -68
  158. data/spec/legion/extensions/llm/registry_publisher_spec.rb +0 -22
  159. data/spec/legion/extensions/llm/responses/response_objects_spec.rb +0 -75
  160. data/spec/legion/extensions/llm/routing/model_offering_spec.rb +0 -281
  161. data/spec/legion/extensions/llm/routing/offering_registry_spec.rb +0 -50
  162. data/spec/legion/extensions/llm/routing/registry_event_spec.rb +0 -120
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b11032eff9bab4fc8c4ad457cd78fdce95f6ec245a5938dbd6ea987729db4238
4
- data.tar.gz: f799901fe6f05201a25237aeafb7779538a995e63e5117e18550935b9e77e12e
3
+ metadata.gz: 6482361b65a465a6eb683fb1789cd47a6a1a80c709c344eefb57205321e90c12
4
+ data.tar.gz: b2dd262d310f469fd1c89fa0b00eb1199822397bdb762c17c056b05cbad1b1d6
5
5
  SHA512:
6
- metadata.gz: d8f610a4c4bb9fdf202c633360c689082f974033e27bdb9bb5c15afa8b609fee313cb36e32109c829b2f6a3613ae64e6859fc962536c804ec3d30f9e47bbc959
7
- data.tar.gz: 82b0d2428591452f708a4fbccd9be04c40752cd3b2a5989e0e0df220406a9e6cfe103da82ad9c5d7123d11875bec97f01ad8c81b83e7ca21deac37b4fde03c98
6
+ metadata.gz: 64cb921e6ccf8361704df0fe4846fb80b3d074ed09b6940f4cff2c2440c8260e36258acda18ed786415d8052a111e33dcd17ad0f1cef1240dd634f14c30f0172
7
+ data.tar.gz: e2bb88cc4d672f4a11a64cdc91cc922964e67eab8baa85ad3108c40cc269363dc8009684162c70d1f98dbf8d0a0757d0ca8790838eb066ddab59a1edf37cb28d
data/CHANGELOG.md CHANGED
@@ -1,5 +1,92 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.8.0 - 2026-08-20
4
+
5
+ **The SSOT v4 contract cut.** This is a complete contract line, not a patch wave:
6
+ Canonical is the only internal language, every authoritative value is created once
7
+ and executed exactly, and one path / one class / one method exists per function.
8
+ The architecture law for the line ships in `RULES.md` at the repository root.
9
+
10
+ ### Added
11
+ - **`RULES.md`** — the 17 architecture laws of the 0.8.x contract line, byte-for-byte
12
+ at the repo root (G5; mirrored to sibling repos at their next releases).
13
+ - **Fleet protocol v3.** `Fleet::Protocol::VERSION = 3`; exact execution only
14
+ (marker required — absence is rejected); explicit `protocol_version` (no default
15
+ fill); one `Fleet::Protocol::REQUIRED_FIELDS` list consumed by both edges;
16
+ `Fleet::ContractError` for envelope/param/message-shape violations; retryability
17
+ derived from the `ProviderOutcome` kind table (contract/policy/auth kinds never
18
+ retry); the response envelope carries the serialized `Canonical::Response` with
19
+ the G5 thinking exclusion performed exactly once at the responder builder.
20
+ - **Conformance kit.** `spec/legion/extensions/llm/conformance/` now carries the
21
+ full 09 oracle: T1-T7 shared examples for all 12 canonical types plus the B1-B9
22
+ (boundary), F1-F7 (fleet), and R1-R5 (registry) shared examples with a kit
23
+ self-test host.
24
+ - **`Canonical::Thinking::Config`** — one name, a `Data` type (the top-level
25
+ `ThinkingConfig` constant and its alias are gone).
26
+ - **`Canonical::Message#metadata`** — unknown keys fold into metadata on every
27
+ canonical type (04 L5); `cache_control` survival through the full JSON wire is
28
+ conformance-pinned (E01).
29
+ - **Strict argument parser** (`Responses::ToolArguments`) — one strict tool-argument
30
+ parse for sync and streaming paths; invalid JSON is a contract error, never a
31
+ fabricated `{}`.
32
+
33
+ ### Changed
34
+ - **Canonical strictification (04 L1-L10).** No nil-returning factories (typed
35
+ `ArgumentError` naming type, member, and offending class); no rescue-and-repair;
36
+ no silent drops; canonical keys only (provider-dialect aliases moved to the
37
+ translator edges, O03a); declared enums validated in both factories; `build` and
38
+ `from_hash` share one normalization path per member; uniform `to_h`/`as_json`/
39
+ `to_json` serialization; G20d (chunk produce-strict / consume-unknown pass-through)
40
+ is stated law.
41
+ - **Provider funnel (05/08).** `complete` enforces canonical messages centrally
42
+ (F2); the `temperature:` kwarg is deleted (temperature lives only in
43
+ `Canonical::Params`, O4); positional `messages` on the callable contract; the
44
+ provider-object fleet dispatch topology is deleted (exact registry dispatch only);
45
+ the legacy offering production path is replaced by the `Registry.snapshot` read
46
+ path (07 C5); one `ensure_configured!` (the Connection duplicate is deleted); one
47
+ health classifier; `normalize_dispatch_error` reads the shared kind table on
48
+ `Routing::ProviderOutcome`; silent rescues now log.
49
+ - **Streaming (02 FL13 / 10 U1-U3).** The streaming path produces
50
+ `Canonical::Chunk` / `Canonical::Response` through the shared `ThinkingExtractor`
51
+ segment core and the strict tool-argument parser, preserving the wire
52
+ index-first / recency-fallback tool-call correlation law.
53
+ - **`FleetLane` queue defaults** come from the one settings home
54
+ (`Llm.default_settings`, `consumer_ack_timeout_ms: 90_000`); the downgraded
55
+ `CredentialSources` paths fail closed (O11).
56
+ - **`Routing::Selection` weight validation** is structural only — a zero component
57
+ (operator disable) is accepted, matching `RecordSupport` (U4).
58
+
59
+ ### Removed
60
+ - **The legacy type set:** `Llm::Message`, `Llm::Chunk`, `Llm::Tokens`,
61
+ `Llm::Thinking` (+ `Thinking::Config`), `Llm::ToolCall`, `Llm::Content`
62
+ (+ `Content::Raw`), `Llm::Attachment`, the `Responses::ChatResponse` /
63
+ `StreamChunk` / `EmbeddingResponse` wrapper family, and `to_internal_h`.
64
+ - **The provider-native facade** (O12): `Llm.chat/context/embed/moderate/paint/
65
+ transcribe/models/providers` entry points, `Chat`, `Agent`, `Context` — zero
66
+ callers in any repo; capability is preserved through the provider base +
67
+ canonical pipeline + fleet.
68
+ - **Legacy offering/lane surface:** `Routing::ModelOffering`, `Routing::OfferingRegistry`,
69
+ `Routing::LaneKey`, `Inventory::ScopedRefresher` (+ `LegacyCoordinatorAdapter` —
70
+ the lex-llm -> legion-llm reverse dependency is gone), the `Llm::Types` alias
71
+ module, `AutoRegistration#discover_instances`/`#provider_aliases`, the
72
+ `UnsupportedCapabilityError` compat alias, `Llm::Aliases`,
73
+ `Taxonomies::OPERATION_ALIASES`/`OPERATION_TO_LANE_TYPE`/`CIRCUIT_STATES`/
74
+ `HEALTH_KEYS`, `Message.wrap`, `Message#to_provider_hash`, `AmbiguousLegacyOfferingError`,
75
+ and the class-level `model_policy`/`resolve_policy_value` policy cascade.
76
+
77
+ ## 0.7.7 - 2026-08-19
78
+
79
+
80
+ ### Added
81
+ - **`Canonical::Message` carries `cache_control`.** Prompt-cache breakpoints are a first-class canonical member (build/from_hash/to_h and the fleet JSON round-trip preserve it) instead of being dropped by member projection.
82
+
83
+ ### Changed
84
+ - **Fleet worker rehydrates wire messages to canonical objects.** `Fleet::WorkerExecution` rebuilds `params[:messages]` into `Canonical::Message` at the wire boundary for chat, stream, and count_tokens dispatch (exact and local-provider paths), so callables receive canonical input only. Non-Hash wire entries raise `ExactOfferingMismatchError`.
85
+
86
+ ### Fixed
87
+ - **Dispatch-boundary contract helper.** `Provider#enforce_canonical_messages!` is the shared loud-reject contract for provider callables: non-canonical message shapes raise `ArgumentError` instead of being coerced or tolerated. `Provider#count_tokens` enforces it and reads canonical content, removing the hash fallback.
88
+ - **Fleet specs assert the canonical boundary.** Worker/responder spec doubles read `.content` off `Canonical::Message` (wire rehydration delivers objects, not hashes).
89
+
3
90
  ## 0.7.6 - 2026-08-19
4
91
 
5
92
  ### Added
data/RULES.md ADDED
@@ -0,0 +1,97 @@
1
+ RULES.md — Legion LLM Architecture Law
2
+ These rules apply to every task, file, repository, agent, model, session, test, refactor, migration, incident, and release.
3
+ The requested task defines what may change. These rules define how the system ALWAYS works.
4
+ Every rule remains active 100% of the time. If requested work conflicts with these rules, stop and surface the conflict before changing code.
5
+ These are architecture laws. Scope, compatibility, urgency, convenience, tests, existing behavior, and model judgment do not change them.
6
+ 1. Canonical is the only internal language.
7
+ Every client translates client wire -> Canonical before shared execution.
8
+ Shared execution carries Canonical through context, tools, routing, direct dispatch, fleet dispatch, and response handling.
9
+ Every provider translates Canonical -> provider wire at the provider boundary, then provider wire -> Canonical before returning to shared execution.
10
+ Every internal boundary validates the Canonical type it is defined to receive and raises immediately when that contract is violated.
11
+ Client Wire -> Client Translator -> Canonical -> Shared Execution -> Canonical -> Provider Translator -> Provider Wire.
12
+ 2. Serialization preserves Canonical.
13
+ Transport may serialize Canonical state. The receiving transport boundary ALWAYS rehydrates the exact Canonical type before execution continues.
14
+ Fleet follows Canonical -> serialize -> wire -> deserialize -> rehydrate Canonical -> Canonical.
15
+ Serialization changes encoding only. Ownership, identity, model, operation, capability, selection, and meaning remain exactly the same.
16
+ After rehydration, shared execution continues only with Canonical objects.
17
+ 3. Every authoritative fact has exactly one owner.
18
+ The owner creates the fact once. Every downstream layer carries, projects, serializes, rehydrates, verifies, or executes that exact fact.
19
+ A downstream layer receiving missing or contradictory authoritative state raises and returns the defect to the owning layer.
20
+ Authority ALWAYS moves forward by preservation.
21
+ Authority is created once and is never recreated downstream.
22
+ 4. Requirements describe the request. Inventory describes reality. Router chooses. Dispatch executes.
23
+ Canonical request construction owns request semantics. RequestRequirements expresses operation, capabilities, modality, context, output, tools, and explicit pins.
24
+ Providers publish exact executable facts into Inventory. Inventory owns canonical instance, offering, lane, capability, context, quota, health, and published weight state.
25
+ Router.next_lane consumes Requirements plus one immutable Inventory snapshot and produces one authoritative Selection.
26
+ Dispatch executes that Selection exactly. Once Selection exists, routing is finished.
27
+ 5. Inventory facts are immutable executable facts.
28
+ Providers publish exact instances and complete offering snapshots through the Inventory publication contract.
29
+ Identity, capability evidence, context evidence, quota domains, availability, and write-time weights are consumed from published Inventory state.
30
+ A changed fact becomes authoritative only through the owning publication or reconciliation path and a new Inventory snapshot.
31
+ Routing reads Inventory. Dispatch verifies and executes Inventory-backed Selection.
32
+ 6. Identity, capabilities, weights, and context policy retain exact ownership.
33
+ Inventory::Identity owns instance, offering, and lane identity; canonical instance identity is provider family plus the operator/configured instance name; physical endpoint data remains secondary.
34
+ Providers publish capability evidence. Requirements state required capabilities. Candidate evaluation compares the two and determines capability eligibility.
35
+ The weight owner computes lane weight at publication time; Inventory stores it; ranking consumes that stored weight; a stored zero disables the lane.
36
+ Preferred-context binning orders eligible candidates into preference bands and preserves eligibility. Capability, health, binning, and weight ALWAYS retain distinct meanings.
37
+ 7. Routing chooses exactly once.
38
+ Router.next_lane is the sole routing authority.
39
+ Candidate evaluation determines eligibility from Requirements and Inventory. Ranking orders eligible candidates from published routing facts.
40
+ Selection freezes the exact provider, instance, offering, lane, model, operation, and routing identity required for execution.
41
+ Every downstream component consumes the Selection it receives.
42
+ Selection is preserved, not reconstructed.
43
+ 8. Exact execution stays exact through every boundary.
44
+ Direct dispatch executes the exact Selection-derived binding it receives.
45
+ Fleet dispatch serializes and signs that exact binding; fleet validation verifies it; fleet rehydration restores it; worker resolution verifies it against authoritative Inventory.
46
+ The selected provider, instance, offering, lane, model, and operation remain identical through projection, signing, transport, validation, rehydration, resolution, and callable invocation.
47
+ A mismatch raises before provider execution.
48
+ An exact execution request ALWAYS remains exact execution.
49
+ 9. Health and errors preserve one authoritative meaning.
50
+ Inventory owns exact-instance availability. An authoritative instance-unavailable result removes that exact instance; readiness probing owns recovery; successful readiness republish re-admits it.
51
+ Overload, timeout, rate limit, model-not-ready, and transient provider failures remain request-local according to ProviderOutcome semantics.
52
+ The first layer that can authoritatively classify an error performs that classification once. Every downstream layer preserves it.
53
+ Programming errors remain programming errors. Contract violations remain contract violations. Routing exhaustion remains the defined typed Rejection.
54
+ 10. Compatibility exists only at explicit edges.
55
+ Supported legacy clients and protocols are translated into the current Canonical and SSOT architecture at explicit compatibility boundaries.
56
+ Shared execution remains Canonical. Routing remains SSOT-driven. Exact execution remains exact.
57
+ Compatibility code adapts an external contract to the current internal architecture.
58
+ The current internal architecture ALWAYS has one representation, one routing authority, one identity system, and one execution truth.
59
+ 11. Fix every defect at its owner.
60
+ Trace the incorrect value to the layer that owns it, then fix that owner.
61
+ Fix client wire in the client translator; Canonical shape in Canonical construction; Requirements in Requirements construction; provider facts in publication; identity in Inventory identity; weights in publication/reconciliation.
62
+ Fix eligibility in candidate evaluation; ordering in ranking; choice in Router.next_lane; execution preservation in dispatch; provider wire in the provider translator.
63
+ The layer where a defect becomes visible is evidence. The owning layer is where the correction belongs.
64
+ 12. A discovered issue remains in its owning domain.
65
+ Complete the requested task inside its stated scope.
66
+ When investigation exposes a separate defect owned by another architectural domain, record and surface it as separate work unless the requested task is explicitly expanded.
67
+ Routing work consumes existing Canonical Requirements and Inventory facts. Canonical work changes Canonical contracts. Provider work changes publication or translation. Transport work changes transport.
68
+ Nearby code never changes ownership. “While we are here” never changes architecture.
69
+ 13. N x N ALWAYS converges through Canonical.
70
+ Equivalent client semantics produce equivalent Canonical state before shared execution. Every provider consumes the same Canonical semantics for the same request.
71
+ When two paths disagree, capture the state at every involved boundary and locate the FIRST point where Canonical meaning diverges.
72
+ Fix that first divergent boundary, then run the exact failing path again.
73
+ Client behavior is proven at client-wire <-> Canonical. Provider behavior is proven at Canonical <-> provider-wire. Shared execution is proven with Canonical throughout.
74
+ 14. Debug from captured authoritative state.
75
+ Capture the actual input at the failing boundary before reasoning from symptoms.
76
+ For translation or transport defects, capture Canonical immediately before and after every involved boundary.
77
+ For routing or dispatch defects, capture Requirements, relevant Inventory facts, Selection, execution binding, and ProviderOutcome.
78
+ Compare each captured value to the contract owned by that layer. Find the first divergence. Fix its owner. Re-run the exact path.
79
+ Then inspect sibling implementations for the same defect class.
80
+ 15. Tests prove the real boundary and the invariant.
81
+ A boundary test exercises the real boundary it claims to protect.
82
+ Fleet tests exercise real serialization, deserialization, Canonical rehydration, signing, validation, exact resolution, and callable dispatch.
83
+ Provider tests exercise the real callable boundary and provider translator. Routing tests exercise real Requirements, Inventory records, candidate evaluation, ranking, and Selection.
84
+ Regression tests prove the violated invariant, not only the observed symptom.
85
+ A green suite is release evidence only when the tested path traverses the real architecture.
86
+ 16. Shared contracts are consumed directly.
87
+ Shared Canonical types own execution representation. Shared Inventory types own inventory state. Shared Routing types own routing state.
88
+ Shared taxonomy owns canonical mappings. Shared ProviderOutcome owns provider-neutral outcomes. Shared fleet protocol owns exact execution claims.
89
+ Every repository consumes these shared owners directly.
90
+ A defect in one shared boundary triggers an audit of every sibling implementation of that boundary. Fix the shared owner centrally whenever the defect belongs to a shared contract.
91
+ 17. Architecture is the release gate.
92
+ Every change preserves every rule in this file.
93
+ Tests, compatibility, historical behavior, migration phase, patch urgency, nearby code, task wording, and model judgment are evaluated UNDER these rules.
94
+ A contradiction between existing behavior and these rules is surfaced as an architecture conflict and resolved at the owning boundary before release.
95
+ Limited scope means do less. Limited scope NEVER means fewer rules apply.
96
+ These rules apply 100% of the time.
97
+ These are the law.
@@ -10,20 +10,11 @@ module Legion
10
10
  # Prerequisites on the extending module:
11
11
  # - `PROVIDER_FAMILY` constant (Symbol, e.g. :ollama)
12
12
  # - `provider_class` singleton method returning the Provider subclass
13
- # DEPRECATED (Phase 4 removal): discover_instances/provider_aliases are kept
14
- # for old consumers; no new registration side effect is added here. Migrated
15
- # provider actors use Inventory::Publisher directly.
13
+ #
14
+ # The legacy discover_instances/provider_aliases defaults are deleted
15
+ # (Phase 4): providers define their own instance discovery, and
16
+ # publication goes through Inventory::Publisher directly.
16
17
  module AutoRegistration
17
- # Override in each provider. Returns { instance_id => config_hash }.
18
- def discover_instances
19
- {}
20
- end
21
-
22
- # Optional provider-family aliases that legion-llm should register
23
- # against the same discovered provider instances.
24
- def provider_aliases
25
- []
26
- end
27
18
  end
28
19
  end
29
20
  end
@@ -7,24 +7,45 @@ module Legion
7
7
  module Canonical
8
8
  # Canonical streaming chunk with full lifecycle support.
9
9
  # Per R4: block_index/item_id/signature lifecycle, multi-tool-call deltas.
10
- # Per G20d: strict on produce, ignore-unknown on consume.
10
+ # Per G20d (04 §11, stated law): strict on produce the named factories
11
+ # and the generic build validate type against CHUNK_TYPES; lenient on
12
+ # consume — from_hash accepts any type symbol and passes it through. # -- required for Data.define block scope
11
13
  Chunk = ::Data.define(
12
14
  :request_id, :conversation_id, :exchange_id,
13
15
  :index, :type, :block_index,
14
16
  :item_id, :delta, :tool_call, :signature,
15
17
  :usage, :stop_reason, :metadata, :timestamp
16
18
  ) do
19
+ # Generic produce path — the only way to construct an arbitrary-type
20
+ # chunk; type is validated against CHUNK_TYPES (G20d).
21
+ def self.build(
22
+ type:, request_id: nil, conversation_id: nil, exchange_id: nil,
23
+ index: nil, block_index: nil, item_id: nil,
24
+ delta: nil, tool_call: nil, signature: nil,
25
+ usage: nil, stop_reason: nil, metadata: {}, timestamp: nil
26
+ )
27
+ type_sym = type.is_a?(::String) ? type.to_sym : type
28
+ Strict.enum!(type_sym, self::CHUNK_TYPES, self::BUILD_SITE, :type)
29
+ new(
30
+ request_id:, conversation_id:, exchange_id:,
31
+ index:, type: type_sym, block_index:,
32
+ item_id:, delta:,
33
+ tool_call: normalize_tool_call!(tool_call, self::BUILD_SITE),
34
+ signature:,
35
+ usage: normalize_usage!(usage, self::BUILD_SITE),
36
+ stop_reason: stop_reason&.to_sym,
37
+ metadata: Strict.metadata!(metadata, self::BUILD_SITE),
38
+ timestamp: timestamp || ::Time.now
39
+ )
40
+ end
41
+
17
42
  # Build a text delta chunk.
18
43
  def self.text_delta(delta:, request_id:, conversation_id: nil, exchange_id: nil,
19
44
  index: 0, block_index: nil, item_id: nil,
20
45
  stop_reason: nil, usage: nil)
21
- new(
22
- type: :text_delta, delta: delta, index: index,
23
- request_id: request_id, conversation_id: conversation_id,
24
- exchange_id: exchange_id, block_index: block_index,
25
- item_id: item_id, tool_call: nil, signature: nil,
26
- usage: usage, stop_reason: stop_reason, metadata: {},
27
- timestamp: ::Time.now
46
+ build(
47
+ type: :text_delta, delta:, request_id:, conversation_id:, exchange_id:,
48
+ index:, block_index:, item_id:, stop_reason:, usage:
28
49
  )
29
50
  end
30
51
 
@@ -32,117 +53,93 @@ module Legion
32
53
  def self.thinking_delta(delta:, request_id:, conversation_id: nil, exchange_id: nil,
33
54
  index: 0, block_index: nil, item_id: nil, signature: nil,
34
55
  stop_reason: nil, usage: nil)
35
- new(
36
- type: :thinking_delta, delta: delta, index: index,
37
- request_id: request_id, conversation_id: conversation_id,
38
- exchange_id: exchange_id, block_index: block_index,
39
- item_id: item_id, tool_call: nil, signature: signature,
40
- usage: usage, stop_reason: stop_reason, metadata: {},
41
- timestamp: ::Time.now
56
+ build(
57
+ type: :thinking_delta, delta:, request_id:, conversation_id:, exchange_id:,
58
+ index:, block_index:, item_id:, signature:, stop_reason:, usage:
42
59
  )
43
60
  end
44
61
 
45
- # Build a tool_call_delta chunk (supports multiple in-flight tool calls via tool_call.id).
62
+ # Build a tool_call_delta chunk (supports multiple in-flight tool calls
63
+ # via the fragment's id/index). tool_call is the delta fragment:
64
+ # { id:, name:, arguments: <String fragment>, index:, signature: }.
46
65
  def self.tool_call_delta(tool_call:, request_id:, conversation_id: nil, exchange_id: nil,
47
66
  index: 0, block_index: nil, item_id: nil,
48
67
  stop_reason: nil, usage: nil)
49
- new(
50
- type: :tool_call_delta, index: index,
51
- request_id: request_id, conversation_id: conversation_id,
52
- exchange_id: exchange_id, block_index: block_index,
53
- item_id: item_id, delta: nil, tool_call: tool_call, signature: nil,
54
- usage: usage, stop_reason: stop_reason, metadata: {},
55
- timestamp: ::Time.now
68
+ build(
69
+ type: :tool_call_delta, tool_call:, request_id:, conversation_id:, exchange_id:,
70
+ index:, block_index:, item_id:, stop_reason:, usage:
56
71
  )
57
72
  end
58
73
 
59
74
  # Build a usage chunk.
60
75
  def self.usage_chunk(usage:, request_id:, conversation_id: nil, exchange_id: nil)
61
- new(
62
- type: :usage, request_id: request_id,
63
- conversation_id: conversation_id, exchange_id: exchange_id,
64
- index: nil, block_index: nil, item_id: nil,
65
- delta: nil, tool_call: nil, signature: nil,
66
- usage: usage, stop_reason: nil, metadata: {},
67
- timestamp: ::Time.now
68
- )
76
+ build(type: :usage, request_id:, conversation_id:, exchange_id:, usage:)
69
77
  end
70
78
 
71
79
  # Build a done chunk.
72
80
  def self.done(request_id:, usage: nil, stop_reason: nil, conversation_id: nil, exchange_id: nil)
73
- new(
74
- type: :done, request_id: request_id,
75
- conversation_id: conversation_id, exchange_id: exchange_id,
76
- index: nil, block_index: nil, item_id: nil,
77
- delta: nil, tool_call: nil, signature: nil,
78
- usage: usage, stop_reason: stop_reason, metadata: {},
79
- timestamp: ::Time.now
80
- )
81
+ build(type: :done, request_id:, usage:, stop_reason:, conversation_id:, exchange_id:)
81
82
  end
82
83
 
83
84
  # Build an error chunk.
84
- def self.error_chunk(error:, request_id:, conversation_id: nil, exchange_id: nil, metadata: nil)
85
- new(
86
- type: :error, request_id: request_id,
87
- conversation_id: conversation_id, exchange_id: exchange_id,
88
- index: nil, block_index: nil, item_id: nil,
89
- delta: nil, tool_call: nil, signature: nil,
90
- usage: nil, stop_reason: :error,
91
- metadata: (metadata || {}).merge(error: error),
92
- timestamp: ::Time.now
85
+ def self.error_chunk(error:, request_id:, conversation_id: nil, exchange_id: nil, metadata: {})
86
+ build(
87
+ type: :error, request_id:, conversation_id:, exchange_id:,
88
+ stop_reason: :error, metadata: metadata.merge(error:)
93
89
  )
94
90
  end
95
91
 
96
- # Build from a Hash (raw provider response or deserialized wire payload).
97
- # Per G20d: ignore-unknown on consume — unknown chunk types are passed through.
98
- def self.from_hash(source)
99
- return nil if source.nil?
100
-
101
- h = source.transform_keys(&:to_sym)
92
+ def self.shape_symbol!(value, site, member)
93
+ return nil if value.nil?
94
+ return value.to_sym if value.is_a?(::String) || value.is_a?(::Symbol)
102
95
 
103
- # Normalize type
104
- type_raw = h.delete(:type)
105
- type_sym = type_raw&.to_sym if type_raw
96
+ raise ArgumentError, "#{site}: #{member} expected String or Symbol, got #{value.class}"
97
+ end
106
98
 
107
- # Normalize nested objects
108
- tool_call_raw = h.delete(:tool_call)
109
- h[:tool_call] = if tool_call_raw.is_a?(ToolCall)
110
- tool_call_raw
111
- elsif tool_call_raw.is_a?(Hash)
112
- ToolCall.from_hash(tool_call_raw)
113
- end
99
+ # Build from a Hash (raw provider response or deserialized wire payload).
100
+ # Per G20d: ignore-unknown on consume — unknown chunk types pass through.
101
+ def self.from_hash(source)
102
+ Strict.require_hash!(source, self::FROM_HASH_SITE)
103
+ hash = Strict.symbolize_keys(source)
104
+ metadata = Strict.fold_unknowns!(self, self::FROM_HASH_SITE, hash)
105
+ type_raw = hash.delete(:type)
106
+ tool_call = normalize_tool_call!(hash.delete(:tool_call), self::FROM_HASH_SITE)
107
+ usage = normalize_usage!(hash.delete(:usage), self::FROM_HASH_SITE)
108
+ stop_reason_raw = hash.delete(:stop_reason)
109
+ timestamp = hash.delete(:timestamp)
110
+ # Remaining keys are all members; pass through with consume defaults.
111
+ new(
112
+ request_id: hash[:request_id],
113
+ conversation_id: hash[:conversation_id],
114
+ exchange_id: hash[:exchange_id],
115
+ index: hash[:index],
116
+ type: type_raw&.to_sym,
117
+ block_index: hash[:block_index],
118
+ item_id: hash[:item_id],
119
+ delta: hash[:delta],
120
+ tool_call:,
121
+ signature: hash[:signature],
122
+ usage:,
123
+ stop_reason: stop_reason_raw&.to_sym,
124
+ metadata:,
125
+ timestamp: timestamp || ::Time.now
126
+ )
127
+ end
114
128
 
115
- usage_raw = h.delete(:usage)
116
- h[:usage] = if usage_raw.is_a?(Usage)
117
- usage_raw
118
- elsif usage_raw.is_a?(Hash)
119
- Usage.from_hash(usage_raw)
120
- end
129
+ # tool_call member: the delta fragment (Hash) or a full ToolCall; nil allowed.
130
+ def self.normalize_tool_call!(tool_call, site)
131
+ return nil if tool_call.nil?
132
+ return tool_call if tool_call.is_a?(::Hash) || tool_call.is_a?(ToolCall)
121
133
 
122
- # Normalize stop_reason
123
- stop_reason_raw = h.delete(:stop_reason) || h.delete(:finish_reason)
124
- h[:stop_reason] = stop_reason_raw&.to_sym if stop_reason_raw
134
+ Strict.expect_type!(tool_call, [::Hash, ToolCall], site, :tool_call)
135
+ end
125
136
 
126
- # Ensure metadata is a Hash
127
- h[:metadata] = h[:metadata] || {}
137
+ def self.normalize_usage!(usage, site)
138
+ return nil if usage.nil?
139
+ return usage if usage.is_a?(Usage)
128
140
 
129
- # Provide defaults for missing fields
130
- new(
131
- request_id: h[:request_id],
132
- conversation_id: h[:conversation_id],
133
- exchange_id: h[:exchange_id],
134
- index: h[:index],
135
- type: type_sym,
136
- block_index: h[:block_index],
137
- item_id: h[:item_id],
138
- delta: h[:delta],
139
- tool_call: h[:tool_call],
140
- signature: h[:signature],
141
- usage: h[:usage],
142
- stop_reason: h[:stop_reason],
143
- metadata: h[:metadata],
144
- timestamp: h[:timestamp] || ::Time.now
145
- )
141
+ Strict.expect_type!(usage, [::Hash], site, :usage)
142
+ Usage.from_hash(usage)
146
143
  end
147
144
 
148
145
  # Serialize to a Hash for AMQP/fleet/wire transport.
@@ -186,9 +183,26 @@ module Legion
186
183
  def content?
187
184
  %i[text_delta thinking_delta].include?(type)
188
185
  end
186
+
187
+ # H1: the single strict constructor. Member shapes are validated
188
+ # (type/stop_reason as String|Symbol, tool_call/usage through the
189
+ # normalizers, metadata as a Hash). Per G20d the TYPE is only
190
+ # shape-checked here — the produce-side enum pin stays in build,
191
+ # and from_hash (consume) passes unknown types through.
192
+ Strict.install_strict_new!(self) do |values, site|
193
+ values[:type] = shape_symbol!(values[:type], site, :type)
194
+ values[:stop_reason] = shape_symbol!(values[:stop_reason], site, :stop_reason)
195
+ values[:tool_call] = normalize_tool_call!(values[:tool_call], site)
196
+ values[:usage] = normalize_usage!(values[:usage], site)
197
+ values[:metadata] = Strict.metadata!(values[:metadata], site)
198
+ values
199
+ end
189
200
  end
190
201
 
191
202
  Chunk::CHUNK_TYPES = %i[text_delta thinking_delta tool_call_delta usage done error].freeze
203
+ Chunk::BUILD_SITE = 'Canonical::Chunk.build'
204
+ Chunk::FROM_HASH_SITE = 'Canonical::Chunk.from_hash'
205
+ Chunk::NEW_SITE = 'Canonical::Chunk.new'
192
206
  end
193
207
  end
194
208
  end