Skip to content

Commit ffe5dba

Browse files
authored
Merge pull request #9236 from kim-em/agent/interval-policy-features
feat(interval): add bounded package policy features
2 parents e82e309 + 0facd23 commit ffe5dba

12 files changed

Lines changed: 740 additions & 4 deletions
Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
/-
2+
Copyright (c) 2026 Lean FRO, LLC. All rights reserved.
3+
Released under Apache 2.0 license as described in the file LICENSE.
4+
Authors: Kim Morrison
5+
-/
6+
7+
module
8+
9+
public import HexInterval.Experiment.Policy
10+
11+
@[expose] public section
12+
13+
/-!
14+
# Bounded package features for interval-search policies
15+
16+
An interval or function package may know that one live offer is more useful
17+
than another, even though the generic scheduler must not know how to interpret
18+
its facts or operations. This experiment gives such packages a stateless,
19+
versioned feature callback. A callback receives one immutable policy snapshot
20+
and one engine-owned offer; the decorator accepts only bounded integer output.
21+
22+
Callbacks are evaluated while a view is decorated; they are never retained in
23+
the policy state. The decorated offer keeps the complete engine-owned offer,
24+
so choosing it still goes through the ordinary freshness and semantic-key
25+
checks. A stale or dishonest feature can therefore change search order, but
26+
cannot authorize an engine transition, enter proof evidence, or bypass replay
27+
validation of the trace that search eventually generates.
28+
-/
29+
30+
namespace Hex.Interval.Experiment.PolicyFeature
31+
32+
open Propagator Propagator.Policy
33+
34+
/-- Versioned identity of one independently registered feature provider.
35+
Changing the meaning of any local feature requires a new provider version. -/
36+
structure ProviderKey where
37+
family : Nat
38+
version : Nat
39+
deriving DecidableEq, Repr
40+
41+
/-- A provider-local feature before its owner is attached by the registry. -/
42+
structure LocalFeature where
43+
key : Nat
44+
value : Int
45+
deriving DecidableEq, Repr
46+
47+
/-- Globally unambiguous feature address and its bounded integer value. -/
48+
structure Feature where
49+
provider : ProviderKey
50+
key : Nat
51+
value : Int
52+
deriving DecidableEq, Repr
53+
54+
/-- Exact immutable input to a feature provider.
55+
56+
The request deliberately contains no package cache and grants no mutation
57+
authority. A goal-oriented provider may close over a fixed frontend goal;
58+
the provider version must change when the interpretation of that configuration
59+
changes. -/
60+
structure Request (Fact : Type) where
61+
scope : ScopeId
62+
serial : Nat
63+
programVersion : Nat
64+
facts : Snapshot Fact
65+
remaining : EngineBudgetView
66+
incomplete : Bool
67+
offer : OfferView
68+
69+
/-- Stateless companion contribution from one interval or frontend package.
70+
Returning an empty array means this provider contributes nothing for the offer. -/
71+
structure Provider (Fact : Type) where
72+
key : ProviderKey
73+
features : Request Fact -> Array LocalFeature
74+
75+
/-- Every accepted output/count dimension which a provider can make large has
76+
an independent cap. Provider identities are opaque registry addresses. -/
77+
structure Limits where
78+
maxProviders : Nat
79+
maxProviderChecks : Nat
80+
maxFeaturesPerProvider : Nat
81+
maxFeaturesPerOffer : Nat
82+
maxTotalFeatures : Nat
83+
maxFeatureKey : Nat
84+
maxFeatureValue : Nat
85+
deriving DecidableEq, Repr
86+
87+
/-- Exact rejection reason. Decoration is transactional: errors return no
88+
partially featured view. -/
89+
inductive Error where
90+
| providerLimit
91+
| duplicateProvider (key : ProviderKey)
92+
| providerCheckLimit
93+
| providerFeatureLimit (provider : ProviderKey) (offer : OfferId)
94+
| offerFeatureLimit (offer : OfferId)
95+
| totalFeatureLimit
96+
| featureKeyLimit (provider : ProviderKey) (key : Nat)
97+
| featureValueLimit (provider : ProviderKey) (key : Nat) (value : Int)
98+
| duplicateFeature (provider : ProviderKey) (key : Nat) (offer : OfferId)
99+
deriving DecidableEq, Repr
100+
101+
/-- Immutable provider order established by successful registry assembly. -/
102+
structure Registry (Fact : Type) where
103+
private mk ::
104+
providers : Array (Provider Fact)
105+
106+
private def makeRegistry (providers : Array (Provider Fact)) : Registry Fact :=
107+
{ providers }
108+
109+
namespace Registry
110+
111+
private def duplicateProvider? (providers : List (Provider Fact)) : Option ProviderKey :=
112+
match providers with
113+
| [] => none
114+
| provider :: rest =>
115+
if rest.any (fun other => other.key == provider.key) then
116+
some provider.key
117+
else
118+
duplicateProvider? rest
119+
120+
/-- Assemble providers in caller order after bounding and checking identities. -/
121+
opaque buildWithin (limits : Limits) (providers : Array (Provider Fact)) :
122+
Except Error (Registry Fact) :=
123+
if limits.maxProviders < providers.size then
124+
.error .providerLimit
125+
else
126+
match duplicateProvider? providers.toList with
127+
| some key => .error (.duplicateProvider key)
128+
| none => .ok (makeRegistry providers)
129+
130+
end Registry
131+
132+
/-- One engine offer decorated with package data. `base` is returned unchanged
133+
when a policy selects this item. -/
134+
structure FeaturedOffer where
135+
base : OfferView
136+
features : Array Feature
137+
138+
/-- Deterministic callback/output counts reported by one successful decoration. -/
139+
structure Metrics where
140+
providerChecks : Nat
141+
emittedFeatures : Nat
142+
deriving DecidableEq, Repr
143+
144+
/-- A policy view plus aligned featured offers. The base view remains the
145+
freshness authority. -/
146+
structure View (Fact : Type) where
147+
base : Policy.View Fact
148+
offers : Array FeaturedOffer
149+
metrics : Metrics
150+
151+
private def request (view : Policy.View Fact) (offer : OfferView) : Request Fact :=
152+
{ scope := view.scope
153+
serial := view.serial
154+
programVersion := view.programVersion
155+
facts := view.facts
156+
remaining := view.remaining
157+
incomplete := view.incomplete
158+
offer }
159+
160+
private def validValue (limit : Nat) (value : Int) : Bool :=
161+
value.natAbs <= limit
162+
163+
/-- Attach package features in stable offer-major, provider-major, local order.
164+
165+
The complete provider/offer cross product is preflight-counted before any
166+
callback is entered. Returned arrays are then checked incrementally against
167+
per-provider, per-offer, whole-view, key, and value limits. -/
168+
opaque decorate (limits : Limits) (registry : Registry Fact)
169+
(view : Policy.View Fact) : Except Error (View Fact) := do
170+
if limits.maxProviders < registry.providers.size then throw .providerLimit
171+
let checks := registry.providers.size * view.offers.size
172+
if limits.maxProviderChecks < checks then throw .providerCheckLimit
173+
let mut decorated := #[]
174+
let mut total := 0
175+
for offer in view.offers do
176+
let mut features := #[]
177+
for provider in registry.providers do
178+
let emitted := provider.features (request view offer)
179+
if limits.maxFeaturesPerProvider < emitted.size then
180+
throw (.providerFeatureLimit provider.key offer.id)
181+
for feature in emitted do
182+
if limits.maxFeatureKey < feature.key then
183+
throw (.featureKeyLimit provider.key feature.key)
184+
if !validValue limits.maxFeatureValue feature.value then
185+
throw (.featureValueLimit provider.key feature.key feature.value)
186+
if features.any (fun old =>
187+
old.provider == provider.key && old.key == feature.key) then
188+
throw (.duplicateFeature provider.key feature.key offer.id)
189+
if limits.maxFeaturesPerOffer <= features.size then
190+
throw (.offerFeatureLimit offer.id)
191+
if limits.maxTotalFeatures <= total then
192+
throw .totalFeatureLimit
193+
features := features.push
194+
{ provider := provider.key, key := feature.key, value := feature.value }
195+
total := total + 1
196+
decorated := decorated.push { base := offer, features }
197+
pure
198+
{ base := view
199+
offers := decorated
200+
metrics := { providerChecks := checks, emittedFeatures := total } }
201+
202+
/-- Exact lookup used by policies which understand a configured feature key.
203+
Unknown features remain inert. -/
204+
def FeaturedOffer.feature? (offer : FeaturedOffer)
205+
(provider : ProviderKey) (key : Nat) : Option Int :=
206+
(offer.features.find? fun feature =>
207+
feature.provider == provider && feature.key == key).map (fun feature => feature.value)
208+
209+
end Hex.Interval.Experiment.PolicyFeature

‎HexInterval/SPEC/hex-interval.md‎

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2958,8 +2958,10 @@ inductive OfferKey
29582958
(reason : SplitReason)
29592959
29602960
structure PolicyFeature where
2961-
key : Nat
2962-
value : Int
2961+
providerFamily : Nat
2962+
providerVersion : Nat
2963+
key : Nat
2964+
value : Int
29632965
29642966
structure ObservationSummary where
29652967
outcome : Nat
@@ -3410,14 +3412,49 @@ distance, mathematical function, or package key. It tests the upgradeable
34103412
feedback seam before domain-specific potential features are admitted through an
34113413
equally bounded interface.
34123414

3415+
The first executable package-feature experiment supplies that interface
3416+
without adding fact or function cases to the scheduler. Independently
3417+
registered companion providers have a numeric family and compatibility
3418+
version. A stateless provider receives one exact immutable policy snapshot and
3419+
one engine-owned offer, and returns provider-local signed integer features; an
3420+
empty result means that provider contributes nothing for the offer. The
3421+
decorator attaches the provider identity to every local key. Package order,
3422+
offer order, and local
3423+
feature order determine one stable output order, while duplicate provider
3424+
identities and duplicate local keys are rejected.
3425+
3426+
The complete provider/offer cross product is preflight-counted before callbacks
3427+
run.
3428+
Independent limits bound provider count, provider checks, features from one
3429+
provider for one offer, features attached to one offer, total features, local
3430+
keys, and absolute feature values. Decoration is transactional: an oversized
3431+
or duplicate result yields no partially featured view. The resulting object
3432+
retains each complete original `OfferView`; a policy can only return that base
3433+
offer through the existing selection path. Thus an inaccurate or stale feature
3434+
can change scheduling and therefore which trace is generated, but it cannot
3435+
authorize a transition, enter proof evidence, bypass replay, or weaken replay
3436+
validation.
3437+
3438+
This is deliberately a companion registry rather than a field added to every
3439+
runtime package. It lets experiments compare interval-width, goal-distance,
3440+
and split-potential vocabularies before fixing their keys or formulas. A
3441+
production consolidation should assemble the feature companions alongside the
3442+
runtime packages and expose the program structure needed for dependency-slice
3443+
features. The callback is pure and accepted decorated output is bounded, but
3444+
the current experiment cannot preempt excessive computation or candidate-array
3445+
allocation inside a callback. Production providers therefore need either a
3446+
restricted bounded builder or an auditable
3447+
declared-work protocol in addition to these output bounds.
3448+
34133449
A live Mathlib-free arbitrary-function canary presents two exponential
34143450
forward contractors and one source split rule in the same frontier. The policy
34153451
selects both fact improvements, then invokes the split probe, then returns its
34163452
split plan; it contains no reference to any of those rule keys. This establishes
34173453
the replaceable staging seam. It does not yet claim that fixed stage order is
34183454
the best policy, nor does it implement width- or goal-sensitive scoring.
34193455

3420-
The prototype computes a goal-directed potential rather than summing raw widths.
3456+
The planned scoring experiment computes a goal-directed potential rather than
3457+
summing raw widths.
34213458
Nodes on the backwards dependency slice from the desired comparison or current
34223459
contradiction receive greater weight. A node's uncertainty records:
34233460

0 commit comments

Comments
 (0)