Skip to main content

eredu_core/
intervention.rs

1//! Immutable, session-bound intervention plans. Tensor selection and prediction
2//! scheduling are independent. Operations execute in their declared list order;
3//! routing operations with potentially overlapping schedules are rejected.
4
5use crate::{capture::*, ObservationPoint, ObservationSupportStatus, TensorAxis};
6use serde::{Deserialize, Serialize};
7use sha2::{Digest, Sha256};
8use std::collections::BTreeSet;
9
10/// Independent wire version for intervention declarations, plans, and outcomes.
11pub const INTERVENTION_SCHEMA_VERSION: u32 = 1;
12/// Hard bound on operations, including inactive operations and their diagnostics.
13pub const MAX_INTERVENTION_OPERATIONS: usize = 64;
14/// Hard bound on compact JSON plan bytes, including replacement values.
15pub const MAX_INTERVENTION_PLAN_BYTES: u64 = 1024 * 1024;
16/// Hard bound on unencoded replacement, mask, bias, and ID storage.
17pub const MAX_INTERVENTION_PAYLOAD_BYTES: u64 = 512 * 1024;
18
19/// Exact activation storage type. Host payloads never authorize a silent cast.
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
21#[serde(rename_all = "snake_case")]
22pub enum InterventionDtype {
23    /// IEEE binary32.
24    Float32,
25    /// IEEE binary16, encoded as exact bits in host payloads.
26    Float16,
27    /// Bfloat16, encoded as exact bits in host payloads.
28    Bfloat16,
29}
30
31/// Complete row-major host values. A preview is never a replacement payload.
32#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
33#[serde(tag = "dtype", content = "values", rename_all = "snake_case")]
34pub enum InterventionValues {
35    /// Finite IEEE binary32 values.
36    Float32(Vec<f32>),
37    /// Finite IEEE binary16 bit patterns.
38    Float16(Vec<u16>),
39    /// Finite bfloat16 bit patterns.
40    Bfloat16(Vec<u16>),
41}
42
43impl InterventionValues {
44    /// Exact dtype encoded by these values.
45    pub fn dtype(&self) -> InterventionDtype {
46        match self {
47            Self::Float32(_) => InterventionDtype::Float32,
48            Self::Float16(_) => InterventionDtype::Float16,
49            Self::Bfloat16(_) => InterventionDtype::Bfloat16,
50        }
51    }
52    /// Number of complete elements.
53    pub fn len(&self) -> usize {
54        match self {
55            Self::Float32(v) => v.len(),
56            Self::Float16(v) | Self::Bfloat16(v) => v.len(),
57        }
58    }
59    /// Whether there are no values.
60    pub fn is_empty(&self) -> bool {
61        self.len() == 0
62    }
63    fn validate(&self) -> Result<(), CaptureError> {
64        let finite = match self {
65            Self::Float32(v) => v.iter().all(|v| v.is_finite()),
66            Self::Float16(v) => v.iter().all(|v| v & 0x7c00 != 0x7c00),
67            Self::Bfloat16(v) => v.iter().all(|v| v & 0x7f80 != 0x7f80),
68        };
69        require(finite, "intervention values must be finite")
70    }
71    fn bytes(&self) -> Result<u64, CaptureError> {
72        mul(
73            self.len() as u64,
74            if self.dtype() == InterventionDtype::Float32 {
75                4
76            } else {
77                2
78            },
79        )
80    }
81}
82
83/// Complete typed tensor with exact selected extents; broadcasting is prohibited.
84#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
85pub struct InterventionTensor {
86    /// Row-major extents, including all selected singleton axes.
87    pub shape: Vec<u64>,
88    /// Complete values in the exact target dtype.
89    pub values: InterventionValues,
90}
91
92/// Distinct stages of an architecture's router. These are not interchangeable.
93#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
94#[serde(rename_all = "snake_case")]
95pub enum RoutingScoreStage {
96    /// Projection output, including the learned projection bias, before scoring.
97    RawLogits,
98    /// Scoring transform output, before selection-only corrections.
99    TransformedScores,
100    /// Scores plus learned selection corrections; affects ranking only.
101    RankingScores,
102}
103
104/// Architecture-declared score transform, independent of native implementation.
105#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
106#[serde(rename_all = "snake_case")]
107pub enum RoutingScoring {
108    /// Softmax across the entire expert axis, then gather selected scores.
109    Softmax,
110    /// Select logits, then softmax across selected slots only.
111    SelectedSoftmax,
112    /// Elementwise sigmoid before selection.
113    Sigmoid,
114    /// Elementwise square root of softplus before selection.
115    SqrtSoftplus,
116}
117
118/// Exact global routed-expert namespace and architecture coefficient policy.
119#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
120pub struct InterventionRoutingPolicy {
121    /// Routed IDs are in `0..expert_count`; shared experts are outside this namespace.
122    pub expert_count: u32,
123    /// Exact number of distinct selected IDs per token row.
124    pub top_k: u32,
125    /// Score transform used by the ordinary router.
126    pub scoring: RoutingScoring,
127    /// Whether gathered scores are normalized before scaling.
128    pub normalize_selected: bool,
129    /// Denominator epsilon used by the architecture.
130    pub normalization_epsilon: f32,
131    /// Architecture's final coefficient multiplier.
132    pub coefficient_scale: f32,
133    /// Number of equal contiguous expert partitions used by group selection.
134    pub groups: u32,
135    /// Maximum eligible groups per token.
136    pub selected_groups: u32,
137    /// Whether a learned per-expert multiplier is applied after normalization.
138    pub learned_coefficient_scale: bool,
139    /// Separately executed shared experts, unchanged by routed operations.
140    pub shared_experts: u32,
141}
142
143/// Execution boundary at which a target becomes mutable.
144#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
145#[serde(rename_all = "snake_case")]
146pub enum InterventionStage {
147    /// After ordinary observation, before the activation is consumed downstream.
148    Activation,
149    /// After ordinary logits observation, before the existing sampler pipeline.
150    LogitsBeforeSampling,
151    /// Router score/ID/coefficient control before any expert provider dispatch.
152    RoutingBeforeDispatch,
153}
154
155/// Operation categories used for exact capability matching.
156#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
157#[serde(rename_all = "snake_case")]
158pub enum InterventionKind {
159    /// Set selected activation elements to zero.
160    Zero,
161    /// Multiply selected activation elements by a finite scalar.
162    Scale,
163    /// Keep or zero individual selected activation elements.
164    Mask,
165    /// Replace every selected element with a complete typed payload.
166    Replace,
167    /// Add a complete typed payload to selected elements, including logit bias.
168    Add,
169    /// Set selected vocabulary IDs to negative infinity before ordinary sampling.
170    MaskLogits,
171    /// Remove routed experts from ranking eligibility and reselect.
172    ExcludeExperts,
173    /// Preserve IDs and set specified coefficients to zero, without renormalizing.
174    ZeroExpertContribution,
175    /// Apply bias at an explicitly supported router score stage.
176    BiasRoutingScores,
177    /// Dispatch exact per-token IDs, obtaining weights from the ordinary router.
178    ForceExperts,
179}
180
181/// A genuine architecture intervention point, separately declared from observations.
182#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
183pub struct InterventionPoint {
184    /// Exact execution target; routing targets identify the routed module.
185    pub path: String,
186    /// Existing logical architecture node identity.
187    pub node_id: String,
188    /// Exact mutable stage.
189    pub stage: InterventionStage,
190    /// Semantic axes in storage order. Routing uses `[token, selected_expert]`.
191    pub axes: Vec<TensorAxis>,
192    /// Exact allowed activation dtypes; empty for routing control.
193    pub dtypes: Vec<InterventionDtype>,
194    /// Operations structurally implemented at this point.
195    pub operations: Vec<InterventionKind>,
196    /// Bias stages implemented by this router; empty for activations.
197    pub score_stages: Vec<RoutingScoreStage>,
198    /// Per-phase support after combining architecture and loaded-session facts.
199    pub prefill: ObservationSupportStatus,
200    /// Per-phase support after combining architecture and loaded-session facts.
201    pub decode: ObservationSupportStatus,
202    /// Additional shape/dtype/execution conditions presented to applications.
203    pub conditions: Vec<String>,
204    /// Present only for pre-dispatch routing points.
205    pub routing: Option<InterventionRoutingPolicy>,
206}
207
208impl InterventionPoint {
209    /// Geometry adapter for shared capture selection and request validation.
210    pub fn observation_geometry(&self) -> ObservationPoint {
211        ObservationPoint {
212            path: self.path.clone(),
213            node_id: self.node_id.clone(),
214            meaning: String::new(),
215            value_type: crate::ObservationValueType::Tensor,
216            dtype: crate::ObservationDtype::Floating,
217            axes: Some(self.axes.clone()),
218            prefill: true,
219            decode: true,
220            requirements: vec![],
221            position: crate::ObservationPosition::BeforeIntervention,
222            retained_bytes: None,
223            host_bytes: None,
224        }
225    }
226}
227
228/// Loaded-session intervention support, retaining exact prepared-source identity.
229#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
230pub struct InterventionDiscovery {
231    /// Intervention wire version.
232    pub schema_version: u32,
233    /// Content-exact identity of the prepared physical source graph.
234    pub artifact_identity: String,
235    /// Identity assigned when a backend session is realized. Cold declarations
236    /// have no session identity and cannot admit nonempty intervention plans.
237    #[serde(default)]
238    pub session_identity: Option<String>,
239    /// Genuine targets with explicit supported, unsupported, or unverified phases.
240    pub points: Vec<InterventionPoint>,
241}
242
243/// Creates a fresh identity when a backend publishes a loaded intervention session.
244/// It is provenance, not a secret or a substitute for completion authority.
245pub fn new_intervention_session_identity() -> String {
246    use std::sync::atomic::{AtomicU64, Ordering};
247    static NEXT: AtomicU64 = AtomicU64::new(0);
248    format!(
249        "intervention-session-{}-{}-{}",
250        std::process::id(),
251        std::time::SystemTime::now()
252            .duration_since(std::time::UNIX_EPOCH)
253            .map_or(0, |d| d.as_nanos()),
254        NEXT.fetch_add(1, Ordering::Relaxed)
255    )
256}
257
258/// Side-effect-free backend facts; absent operations or dtypes are unsupported.
259#[derive(Debug, Clone, Default)]
260pub struct InterventionMechanisms {
261    /// Implemented native operation categories, independent of model family.
262    pub operations: Vec<InterventionKind>,
263    /// Exact floating dtypes supported by native activation arithmetic.
264    pub dtypes: Vec<InterventionDtype>,
265    /// Router stages whose biases the backend implements.
266    pub score_stages: Vec<RoutingScoreStage>,
267}
268
269/// One typed mutation. No variant contains native handles or executable code.
270#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
271#[serde(tag = "kind", rename_all = "snake_case")]
272pub enum InterventionAction {
273    /// Zero selected elements; retains dtype and shape.
274    Zero {
275        /// Required runtime storage dtype.
276        dtype: InterventionDtype,
277    },
278    /// Native multiplication; the finite scalar must be representable in the dtype.
279    Scale {
280        /// Required runtime storage dtype.
281        dtype: InterventionDtype,
282        /// Finite scalar, with native dtype rounding.
283        factor: f32,
284    },
285    /// False entries become zero; shape must exactly equal the selected region.
286    Mask {
287        /// Required runtime storage dtype.
288        dtype: InterventionDtype,
289        /// Complete selected extents.
290        shape: Vec<u64>,
291        /// Row-major keep bits.
292        keep: Vec<bool>,
293    },
294    /// Whole-value replacement or patching according to the explicit axis slices.
295    Replace {
296        /// Complete exact-dtype selected values.
297        tensor: InterventionTensor,
298    },
299    /// Elementwise addition; supports typed logit bias without a second sampler.
300    Add {
301        /// Complete exact-dtype selected values.
302        tensor: InterventionTensor,
303    },
304    /// Negative-infinity mask at final logits; selection must retain full vocabulary.
305    MaskLogits {
306        /// Required runtime storage dtype.
307        dtype: InterventionDtype,
308        /// Unique canonical vocabulary IDs.
309        token_ids: Vec<u32>,
310    },
311    /// Eligibility exclusion before the architecture's existing selection algorithm.
312    /// Gathered scores retain ordinary normalization and scaling semantics.
313    ExcludeExperts {
314        /// Unique global routed-expert IDs.
315        expert_ids: Vec<u32>,
316    },
317    /// Suppress selected contributions without reselection or renormalization.
318    /// Remaining coefficients retain their original magnitude. Expert computation
319    /// is not promised to be avoided. Shared experts remain unchanged.
320    ZeroExpertContribution {
321        /// Unique global routed-expert IDs.
322        expert_ids: Vec<u32>,
323    },
324    /// Per-expert bias replicated explicitly over the selected token rows.
325    BiasRoutingScores {
326        /// Precise scoring boundary.
327        stage: RoutingScoreStage,
328        /// Unique global routed-expert IDs.
329        expert_ids: Vec<u32>,
330        /// One finite additive bias per listed expert.
331        biases: Vec<f32>,
332    },
333    /// Replace selected IDs before dispatch. Scores/weights are gathered from the
334    /// architecture's router, preserving its normalization, scaling and learned
335    /// multipliers. There is no caller coefficient override.
336    ForceExperts {
337        /// Exact `[selected_token_rows, top_k]` extents.
338        shape: [u64; 2],
339        /// Complete row-major global routed-expert IDs.
340        expert_ids: Vec<u32>,
341    },
342}
343
344impl InterventionAction {
345    /// Revalidates complete host parameters against an exact native selected region.
346    pub fn validate_activation_region(
347        &self,
348        dtype: InterventionDtype,
349        shape: &[u64],
350    ) -> Result<(), CaptureError> {
351        require(
352            self.dtype() == Some(dtype),
353            "intervention runtime dtype differs from exact declared dtype",
354        )?;
355        require(
356            !shape.is_empty() && shape.len() <= 32 && !shape.contains(&0),
357            "invalid activation region shape",
358        )?;
359        validate_activation_parameters(self, shape.last().and_then(|n| u32::try_from(*n).ok()))?;
360        validate_payload_shape(self, shape)
361    }
362    /// Exact operation category.
363    pub fn kind(&self) -> InterventionKind {
364        match self {
365            Self::Zero { .. } => InterventionKind::Zero,
366            Self::Scale { .. } => InterventionKind::Scale,
367            Self::Mask { .. } => InterventionKind::Mask,
368            Self::Replace { .. } => InterventionKind::Replace,
369            Self::Add { .. } => InterventionKind::Add,
370            Self::MaskLogits { .. } => InterventionKind::MaskLogits,
371            Self::ExcludeExperts { .. } => InterventionKind::ExcludeExperts,
372            Self::ZeroExpertContribution { .. } => InterventionKind::ZeroExpertContribution,
373            Self::BiasRoutingScores { .. } => InterventionKind::BiasRoutingScores,
374            Self::ForceExperts { .. } => InterventionKind::ForceExperts,
375        }
376    }
377    /// Exact activation dtype, absent for architecture-owned routing arithmetic.
378    pub fn dtype(&self) -> Option<InterventionDtype> {
379        match self {
380            Self::Zero { dtype }
381            | Self::Scale { dtype, .. }
382            | Self::Mask { dtype, .. }
383            | Self::MaskLogits { dtype, .. } => Some(*dtype),
384            Self::Replace { tensor } | Self::Add { tensor } => Some(tensor.values.dtype()),
385            _ => None,
386        }
387    }
388    fn payload_bytes(&self) -> Result<u64, CaptureError> {
389        match self {
390            Self::Replace { tensor } | Self::Add { tensor } => {
391                add(mul(tensor.shape.len() as u64, 8)?, tensor.values.bytes()?)
392            }
393            Self::Mask { shape, keep, .. } => add(mul(shape.len() as u64, 8)?, keep.len() as u64),
394            Self::MaskLogits { token_ids, .. } => mul(token_ids.len() as u64, 4),
395            Self::ExcludeExperts { expert_ids }
396            | Self::ZeroExpertContribution { expert_ids }
397            | Self::ForceExperts { expert_ids, .. } => mul(expert_ids.len() as u64, 4),
398            Self::BiasRoutingScores {
399                expert_ids, biases, ..
400            } => mul(add(expert_ids.len() as u64, biases.len() as u64)?, 4),
401            _ => Ok(0),
402        }
403    }
404}
405
406/// Bounded before/after evidence. Both sides are charged to the shared capture
407/// ledger. Routing evidence includes selected IDs and coefficients, not a second
408/// unmodified model forward pass.
409#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
410#[serde(tag = "kind", rename_all = "snake_case")]
411pub enum InterventionEvidence {
412    /// Only the attributed application outcome is recorded.
413    None,
414    /// Bounded row-major values per side/route field.
415    Preview {
416        /// Positive maximum number of elements per field.
417        max_elements: u64,
418    },
419    /// Finite statistics of activation values on each side.
420    Summary,
421}
422
423/// One exact operation. List order is deterministic for activation composition.
424#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
425pub struct InterventionOperation {
426    /// Unique, nonempty caller identity, at most 128 UTF-8 bytes.
427    pub id: String,
428    /// Exact path from loaded-session intervention discovery.
429    pub target: String,
430    /// Prediction schedule; does not select prompt tensor rows.
431    pub schedule: CaptureSchedule,
432    /// Explicit within-tensor semantic slices. Unmentioned axes remain whole.
433    pub slices: Vec<CaptureSlice>,
434    /// Typed operation and complete bounded payload.
435    pub action: InterventionAction,
436    /// Requested bounded evidence, charged in addition to ordinary captures.
437    pub evidence: InterventionEvidence,
438}
439
440/// Serializable request. Only admission produces an executable immutable plan.
441#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
442pub struct InterventionPlan {
443    /// Intervention wire version.
444    pub schema_version: u32,
445    /// Activation operations compose in list order; routing overlaps are rejected.
446    pub operations: Vec<InterventionOperation>,
447}
448
449impl InterventionPlan {
450    /// Empty plan preserves the ordinary path and sampling/RNG behavior.
451    pub fn none() -> Self {
452        Self {
453            schema_version: INTERVENTION_SCHEMA_VERSION,
454            operations: vec![],
455        }
456    }
457
458    /// Validates all known constraints without creating or evaluating native values.
459    /// The facade supplies its loaded session identity, never an application catalog.
460    pub fn admit(
461        self,
462        discovery: &InterventionDiscovery,
463        request: CaptureRequestShape,
464        session_id: &str,
465    ) -> Result<AdmittedInterventionPlan, CaptureError> {
466        require(
467            self.schema_version == INTERVENTION_SCHEMA_VERSION
468                && discovery.schema_version == INTERVENTION_SCHEMA_VERSION,
469            "unsupported intervention schema",
470        )?;
471        require(
472            !session_id.is_empty() && !discovery.artifact_identity.is_empty(),
473            "missing intervention session/source identity",
474        )?;
475        require(
476            discovery
477                .session_identity
478                .as_ref()
479                .is_some_and(|id| !id.is_empty()),
480            "intervention admission requires a realized backend session",
481        )?;
482        require(
483            request.batch > 0 && request.prompt_tokens > 0 && request.max_predictions > 0,
484            "empty intervention request geometry",
485        )?;
486        add(request.prompt_tokens, request.max_predictions)?;
487        mul(request.batch, request.prompt_tokens)?;
488        require(
489            self.operations.len() <= MAX_INTERVENTION_OPERATIONS,
490            "too many intervention operations",
491        )?;
492        let mut payload_bytes = 0;
493        let mut ids = BTreeSet::new();
494        let mut points = Vec::new();
495        for operation in &self.operations {
496            require(
497                !operation.id.is_empty() && operation.id.len() <= 128 && ids.insert(&operation.id),
498                "intervention IDs must be unique and contain 1..=128 bytes",
499            )?;
500            require(
501                operation.target.len() <= 1024,
502                "intervention target exceeds metadata bound",
503            )?;
504            payload_bytes = add(payload_bytes, operation.action.payload_bytes()?)?;
505            require(
506                payload_bytes <= MAX_INTERVENTION_PAYLOAD_BYTES,
507                "intervention payload exceeds hard bound",
508            )?;
509            let mut matching = discovery
510                .points
511                .iter()
512                .filter(|p| p.path == operation.target);
513            let point = matching
514                .next()
515                .ok_or_else(|| CaptureError::MissingPath(operation.target.clone()))?;
516            require(
517                matching.next().is_none(),
518                "ambiguous intervention target declaration",
519            )?;
520            validate_operation(operation, point, request)?;
521            points.push(point.clone());
522        }
523        for (index, (operation, point)) in self.operations.iter().zip(&points).enumerate() {
524            if point.routing.is_none() {
525                continue;
526            }
527            for previous in &self.operations[..index] {
528                if previous.target != operation.target {
529                    continue;
530                }
531                for phase in [CapturePhase::Prefill, CapturePhase::Decode] {
532                    let a = previous
533                        .schedule
534                        .count_and_last(phase, request.max_predictions)?;
535                    let b = operation
536                        .schedule
537                        .count_and_last(phase, request.max_predictions)?;
538                    if let (Some((ac, al)), Some((bc, bl))) = (a, b) {
539                        let af = al - (ac - 1) * previous.schedule.every;
540                        let bf = bl - (bc - 1) * operation.schedule.every;
541                        require(
542                            al < bf || bl < af,
543                            "routing operations have potentially overlapping schedules",
544                        )?;
545                    }
546                }
547            }
548        }
549        let mut encoded = PlanCounter(0);
550        serde_json::to_writer(&mut encoded, &self)
551            .map_err(|_| CaptureError::Invalid("intervention plan exceeds encoded bound".into()))?;
552        let digest = Sha256::digest(
553            serde_json::to_vec(&(
554                &self,
555                &points,
556                request,
557                &discovery.artifact_identity,
558                &discovery.session_identity,
559                session_id,
560            ))
561            .map_err(|e| CaptureError::Invalid(e.to_string()))?,
562        );
563        let identity = format!(
564            "intervention-v1-{}",
565            digest
566                .iter()
567                .map(|byte| format!("{byte:02x}"))
568                .collect::<String>()
569        );
570        Ok(AdmittedInterventionPlan {
571            plan: self,
572            points,
573            request,
574            identity,
575            artifact_identity: discovery.artifact_identity.clone(),
576            session_id: session_id.into(),
577        })
578    }
579}
580
581struct PlanCounter(u64);
582impl std::io::Write for PlanCounter {
583    fn write(&mut self, bytes: &[u8]) -> std::io::Result<usize> {
584        self.0 = self
585            .0
586            .checked_add(bytes.len() as u64)
587            .filter(|n| *n <= MAX_INTERVENTION_PLAN_BYTES)
588            .ok_or_else(|| std::io::Error::other("intervention plan bound"))?;
589        Ok(bytes.len())
590    }
591    fn flush(&mut self) -> std::io::Result<()> {
592        Ok(())
593    }
594}
595
596/// Immutable admission proof. It cannot be deserialized or edited during a run.
597#[derive(Debug, Clone)]
598pub struct AdmittedInterventionPlan {
599    plan: InterventionPlan,
600    points: Vec<InterventionPoint>,
601    request: CaptureRequestShape,
602    identity: String,
603    artifact_identity: String,
604    session_id: String,
605}
606
607impl AdmittedInterventionPlan {
608    /// Stable plan/semantics/request/source/session digest.
609    pub fn identity(&self) -> &str {
610        &self.identity
611    }
612    /// Exact prepared-source identity at admission.
613    pub fn artifact_identity(&self) -> &str {
614        &self.artifact_identity
615    }
616    /// Exact loaded facade session at admission.
617    pub fn session_id(&self) -> &str {
618        &self.session_id
619    }
620    /// Immutable operations in composition order.
621    pub fn plan(&self) -> &InterventionPlan {
622        &self.plan
623    }
624    /// Selected declarations in operation order.
625    pub fn points(&self) -> &[InterventionPoint] {
626        &self.points
627    }
628    /// Admitted prompt and prediction geometry.
629    pub fn request(&self) -> CaptureRequestShape {
630        self.request
631    }
632    /// Whether the ordinary path can omit intervention machinery.
633    pub fn is_empty(&self) -> bool {
634        self.plan.operations.is_empty()
635    }
636    /// Validates request-dependent shape, exact dtype and payload geometry before
637    /// applying one scheduled operation. This does not assert prior cache rollback.
638    pub fn validate_actual(
639        &self,
640        index: usize,
641        phase: CapturePhase,
642        prediction: u64,
643        shape: &[u64],
644        dtype: Option<InterventionDtype>,
645    ) -> Result<ResolvedCaptureSlice, CaptureError> {
646        let operation = self
647            .plan
648            .operations
649            .get(index)
650            .ok_or_else(|| CaptureError::Invalid("invalid admitted operation index".into()))?;
651        require(
652            prediction < self.request.max_predictions
653                && operation.schedule.includes(phase, prediction),
654            "inactive intervention operation",
655        )?;
656        require(
657            operation.action.dtype() == dtype,
658            "intervention runtime dtype differs from exact declared dtype",
659        )?;
660        let point = &self.points[index];
661        let geometry = point.observation_geometry();
662        self.request
663            .validate_actual(&geometry, phase, prediction, shape)?;
664        let slice = operation.resolve_slice(point, shape)?;
665        validate_payload_shape(&operation.action, &slice.shape)?;
666        Ok(slice)
667    }
668}
669
670impl InterventionOperation {
671    /// Resolves semantic selection with the same half-open/stride rules as capture.
672    pub fn resolve_slice(
673        &self,
674        point: &InterventionPoint,
675        shape: &[u64],
676    ) -> Result<ResolvedCaptureSlice, CaptureError> {
677        resolve_slice(
678            &point.observation_geometry(),
679            &CaptureSelection {
680                id: self.id.clone(),
681                path: self.target.clone(),
682                schedule: self.schedule.clone(),
683                slices: self.slices.clone(),
684                transform: CaptureTransform::FullTensor,
685            },
686            shape,
687        )
688    }
689}
690
691fn validate_operation(
692    operation: &InterventionOperation,
693    point: &InterventionPoint,
694    request: CaptureRequestShape,
695) -> Result<(), CaptureError> {
696    require(
697        point.node_id.len() <= 1024 && !point.node_id.is_empty(),
698        "invalid intervention node identity",
699    )?;
700    require(
701        !point.axes.is_empty() && point.axes.len() <= 32,
702        "intervention requires declared bounded tensor rank",
703    )?;
704    require(
705        point.operations.contains(&operation.action.kind()),
706        "operation unsupported at intervention point",
707    )?;
708    require(
709        operation.schedule.every != 0
710            && operation
711                .schedule
712                .end_prediction
713                .is_none_or(|end| end > operation.schedule.first_prediction),
714        "invalid intervention schedule",
715    )?;
716    let mut axes = BTreeSet::new();
717    for axis in &point.axes {
718        require(
719            axes.insert(&axis.name),
720            "ambiguous intervention axis declaration",
721        )?;
722    }
723    let mut selected = BTreeSet::new();
724    for slice in &operation.slices {
725        require(
726            selected.insert(&slice.axis) && axes.contains(&slice.axis),
727            "duplicate or unknown intervention axis",
728        )?;
729        require(
730            slice.stride > 0 && slice.start < slice.end,
731            "intervention slices must be nonempty with positive stride",
732        )?;
733        if point.routing.is_some() {
734            require(
735                slice.axis == "token",
736                "routing permits token-row selection only",
737            )?;
738        }
739        if matches!(operation.action, InterventionAction::MaskLogits { .. }) {
740            require(
741                slice.axis != "vocabulary",
742                "logit masks require the complete vocabulary axis",
743            )?;
744        }
745    }
746    if let Some(dtype) = operation.action.dtype() {
747        require(
748            point.routing.is_none() && point.dtypes.contains(&dtype),
749            "activation dtype unsupported at target",
750        )?;
751    } else {
752        require(
753            point.routing.is_some(),
754            "routing action requires a pre-dispatch routing point",
755        )?;
756    }
757    match operation.evidence {
758        InterventionEvidence::Preview { max_elements } => require(
759            max_elements > 0 && max_elements <= 4096,
760            "intervention preview requires 1..=4096 elements",
761        )?,
762        InterventionEvidence::Summary => require(
763            point.routing.is_none(),
764            "routing evidence requires an ID/coefficient preview",
765        )?,
766        InterventionEvidence::None => (),
767    }
768    validate_action(&operation.action, point)?;
769    for (phase, status) in [
770        (CapturePhase::Prefill, &point.prefill),
771        (CapturePhase::Decode, &point.decode),
772    ] {
773        if let Some((_, last)) = operation
774            .schedule
775            .count_and_last(phase, request.max_predictions)?
776        {
777            if *status != ObservationSupportStatus::Supported {
778                return Err(CaptureError::Unsupported(format!(
779                    "intervention {} {phase:?}: {status:?}",
780                    point.path
781                )));
782            }
783            let geometry = point.observation_geometry();
784            if let Some(shape) = request.resolve(&geometry, phase, last)? {
785                let slice = operation.resolve_slice(point, &shape)?;
786                validate_payload_shape(&operation.action, &slice.shape)?;
787            }
788        }
789    }
790    Ok(())
791}
792
793fn validate_activation_parameters(
794    action: &InterventionAction,
795    vocabulary: Option<u32>,
796) -> Result<(), CaptureError> {
797    use InterventionAction as A;
798    match action {
799        A::Scale { dtype, factor } => {
800            let maximum = match dtype {
801                InterventionDtype::Float16 => 65504.0,
802                InterventionDtype::Bfloat16 => f32::from_bits(0x7f7f0000),
803                InterventionDtype::Float32 => f32::MAX,
804            };
805            require(
806                factor.is_finite() && factor.abs() <= maximum,
807                "scale must be finite and representable in the target dtype",
808            )?;
809        }
810        A::Replace { tensor } | A::Add { tensor } => {
811            require(
812                !tensor.shape.is_empty() && tensor.shape.len() <= 32 && !tensor.shape.contains(&0),
813                "invalid replacement tensor rank/extents",
814            )?;
815            require(
816                elements(&tensor.shape)? == tensor.values.len() as u64,
817                "replacement tensor is incomplete or has malformed shape",
818            )?;
819            tensor.values.validate()?;
820        }
821        A::Mask { shape, keep, .. } => {
822            require(
823                !shape.is_empty() && shape.len() <= 32 && !shape.contains(&0),
824                "invalid mask rank/extents",
825            )?;
826            require(
827                elements(shape)? == keep.len() as u64,
828                "mask shape differs from complete keep values",
829            )?;
830        }
831        A::MaskLogits { token_ids, .. } => {
832            let vocabulary = vocabulary.ok_or_else(|| {
833                CaptureError::Unsupported("logit mask requires known vocabulary extent".into())
834            })?;
835            validate_ids(token_ids, vocabulary)?;
836            require(
837                token_ids.len() < vocabulary as usize,
838                "logit mask cannot exclude the whole vocabulary",
839            )?;
840        }
841        A::Zero { .. } => (),
842        _ => {
843            return Err(CaptureError::Invalid(
844                "routing action cannot replace an activation".into(),
845            ))
846        }
847    }
848    Ok(())
849}
850
851fn validate_action(
852    action: &InterventionAction,
853    point: &InterventionPoint,
854) -> Result<(), CaptureError> {
855    use InterventionAction as A;
856    if action.dtype().is_some() {
857        let vocabulary = if matches!(action, A::MaskLogits { .. }) {
858            require(
859                point.stage == InterventionStage::LogitsBeforeSampling,
860                "logit masking requires final logits",
861            )?;
862            point
863                .axes
864                .iter()
865                .find(|axis| axis.name == "vocabulary")
866                .and_then(|axis| match axis.dimension {
867                    crate::SymbolicDimension::Known(n) => u32::try_from(n).ok(),
868                    _ => None,
869                })
870        } else {
871            None
872        };
873        return validate_activation_parameters(action, vocabulary);
874    }
875
876    match action {
877        A::ExcludeExperts { expert_ids }
878        | A::ZeroExpertContribution { expert_ids }
879        | A::BiasRoutingScores { expert_ids, .. }
880        | A::ForceExperts { expert_ids, .. } => {
881            let policy = point
882                .routing
883                .as_ref()
884                .ok_or_else(|| CaptureError::Invalid("missing routing policy".into()))?;
885            require(
886                point.stage == InterventionStage::RoutingBeforeDispatch,
887                "routing control requires pre-dispatch target",
888            )?;
889            require(
890                policy.expert_count > 0
891                    && policy.top_k > 0
892                    && policy.top_k <= policy.expert_count
893                    && policy.groups > 0
894                    && policy.expert_count % policy.groups == 0
895                    && policy.selected_groups > 0
896                    && policy.selected_groups <= policy.groups
897                    && policy.top_k
898                        <= policy.selected_groups * (policy.expert_count / policy.groups)
899                    && policy.normalization_epsilon.is_finite()
900                    && policy.normalization_epsilon >= 0.0
901                    && policy.coefficient_scale.is_finite()
902                    && policy.coefficient_scale > 0.0,
903                "invalid routing declaration",
904            )?;
905            if let A::ForceExperts { shape, .. } = action {
906                require(
907                    shape[0] > 0
908                        && shape[1] == policy.top_k as u64
909                        && elements(shape)? == expert_ids.len() as u64,
910                    "forced route shape must be [selected_token_rows, top_k]",
911                )?;
912                for row in expert_ids.chunks(policy.top_k as usize) {
913                    validate_ids(row, policy.expert_count)?;
914                    let width = policy.expert_count / policy.groups;
915                    let groups: BTreeSet<_> = row.iter().map(|id| id / width).collect();
916                    require(
917                        groups.len() <= policy.selected_groups as usize,
918                        "forced IDs exceed architecture's selected-group count",
919                    )?;
920                }
921            } else {
922                validate_ids(expert_ids, policy.expert_count)?;
923            }
924            if let A::ExcludeExperts { .. } = action {
925                require(
926                    policy.expert_count as usize - expert_ids.len() >= policy.top_k as usize,
927                    "excluded experts make top-k infeasible",
928                )?;
929                // Any group the unchanged algorithm can choose must have enough
930                // eligible members. This conservative condition prevents -inf IDs
931                // from silently entering a dispatch after group selection.
932                let width = policy.expert_count / policy.groups;
933                let mut available = vec![width; policy.groups as usize];
934                for id in expert_ids {
935                    available[(id / width) as usize] -= 1;
936                }
937                available.sort_unstable();
938                require(
939                    available
940                        .iter()
941                        .take(policy.selected_groups as usize)
942                        .sum::<u32>()
943                        >= policy.top_k,
944                    "exclusion cannot guarantee top-k within selected groups",
945                )?;
946            }
947            if let A::BiasRoutingScores { stage, biases, .. } = action {
948                require(
949                    point.score_stages.contains(stage),
950                    "router bias stage unsupported",
951                )?;
952                require(
953                    biases.len() == expert_ids.len() && biases.iter().all(|b| b.is_finite()),
954                    "router bias must have one finite value per expert",
955                )?;
956            }
957        }
958        _ => unreachable!("activation actions validated above"),
959    }
960    Ok(())
961}
962
963fn validate_payload_shape(
964    action: &InterventionAction,
965    selected: &[u64],
966) -> Result<(), CaptureError> {
967    let expected: Option<&[u64]> = match action {
968        InterventionAction::Mask { shape, .. } => Some(shape),
969        InterventionAction::Replace { tensor } | InterventionAction::Add { tensor } => {
970            Some(&tensor.shape)
971        }
972        InterventionAction::ForceExperts { shape, .. } => Some(shape),
973        _ => None,
974    };
975    require(!selected.contains(&0), "empty intervention selection")?;
976    require(
977        expected.is_none_or(|expected| expected == selected),
978        "intervention payload must exactly match selected shape; broadcasting is prohibited",
979    )
980}
981
982fn validate_ids(ids: &[u32], count: u32) -> Result<(), CaptureError> {
983    require(!ids.is_empty(), "intervention ID list must be nonempty")?;
984    let mut unique = BTreeSet::new();
985    require(
986        ids.iter().all(|id| *id < count && unique.insert(*id)),
987        "intervention IDs are duplicate or out of range",
988    )
989}
990
991fn require(condition: bool, message: &str) -> Result<(), CaptureError> {
992    if condition {
993        Ok(())
994    } else {
995        Err(CaptureError::Invalid(message.into()))
996    }
997}
998
999/// Actual per-operation application outcome, independent of generation success.
1000#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1001#[serde(tag = "kind", rename_all = "snake_case")]
1002pub enum InterventionOutcome {
1003    /// The phase/prediction was not selected by this operation's schedule.
1004    Inactive,
1005    /// The replacement/effective routes were passed downstream.
1006    Applied,
1007    /// The scheduled point was unexpectedly absent from the forward pass.
1008    Missing,
1009    /// This operation failed; earlier operations or cache work may already have run.
1010    Failed {
1011        /// Bounded human diagnostic.
1012        message: String,
1013    },
1014}
1015
1016/// Bounded attributed event. Large plan payloads are never repeated in events.
1017#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1018pub struct InterventionRecord {
1019    /// Intervention wire version.
1020    pub schema_version: u32,
1021    /// Immutable admitted plan identity.
1022    pub plan_id: String,
1023    /// Caller operation identity.
1024    pub operation_id: String,
1025    /// Exact execution target.
1026    pub target: String,
1027    /// Existing logical architecture node identity.
1028    pub node_id: String,
1029    /// Actual forward phase.
1030    pub phase: CapturePhase,
1031    /// Capture-compatible prediction index.
1032    pub prediction_index: u64,
1033    /// Actual application status.
1034    pub outcome: InterventionOutcome,
1035    /// Optional bounded pre/post activation or route-field captures. Position
1036    /// identifies original/effective values at this exact operation boundary.
1037    pub evidence: Vec<CaptureRecord>,
1038    /// Diagnostic reservation in the shared capture ledger; evidence owns its charges.
1039    pub charged: CaptureUsage,
1040}
1041
1042/// Native activation primitives. Portable runtime code owns operation dispatch,
1043/// payload validation, region selection/update order, and composition.
1044pub trait InterventionBackend: CaptureBackend {
1045    /// Exact dtype without evaluating the native value.
1046    fn intervention_dtype(&self, tensor: &Self::Tensor) -> Result<InterventionDtype, Self::Error>;
1047    /// Checks backend indexing/shape limits without allocating or evaluating tensors.
1048    fn validate_intervention_geometry(
1049        &self,
1050        source: &[u64],
1051        slice: &ResolvedCaptureSlice,
1052    ) -> Result<(), CaptureError>;
1053    /// Selects the exact positive-stride region, preserving singleton dimensions.
1054    fn select_region(
1055        &mut self,
1056        tensor: &Self::Tensor,
1057        slice: &ResolvedCaptureSlice,
1058    ) -> Result<Self::Tensor, Self::Error>;
1059    /// Returns a value with only the selected region replaced; preserves the source.
1060    fn update_region(
1061        &mut self,
1062        tensor: &Self::Tensor,
1063        slice: &ResolvedCaptureSlice,
1064        replacement: &Self::Tensor,
1065    ) -> Result<Self::Tensor, Self::Error>;
1066    /// Native zeros with the supplied exact shape/dtype.
1067    fn zeros(
1068        &mut self,
1069        shape: &[u64],
1070        dtype: InterventionDtype,
1071    ) -> Result<Self::Tensor, Self::Error>;
1072    /// Multiplies by a scalar explicitly rounded to the value's dtype.
1073    fn scale(&mut self, value: &Self::Tensor, factor: f32) -> Result<Self::Tensor, Self::Error>;
1074    /// Keeps true elements and fills false elements with an exact native-dtype scalar.
1075    fn fill_masked(
1076        &mut self,
1077        value: &Self::Tensor,
1078        keep: &[bool],
1079        fill: f32,
1080    ) -> Result<Self::Tensor, Self::Error>;
1081    /// Uploads a complete tensor with exact dtype, including f16/bf16 bit patterns.
1082    fn realize_tensor(&mut self, tensor: &InterventionTensor) -> Result<Self::Tensor, Self::Error>;
1083    /// Adds tensors of identical shape and dtype, without broadcasting.
1084    fn add(
1085        &mut self,
1086        left: &Self::Tensor,
1087        right: &Self::Tensor,
1088    ) -> Result<Self::Tensor, Self::Error>;
1089    /// Fills explicit last-axis columns over all rows of this selected region.
1090    fn fill_columns(
1091        &mut self,
1092        value: &Self::Tensor,
1093        ids: &[u32],
1094        fill: f32,
1095    ) -> Result<Self::Tensor, Self::Error>;
1096}
1097
1098/// Cold backend facts used by both admission and execution reservation. Unknown
1099/// estimates must return an error; they must never be represented as zero cost.
1100pub trait InterventionEstimator: Send + Sync {
1101    /// Additional native indexing constraints, beyond portable slice validation.
1102    fn validate_geometry(
1103        &self,
1104        source: &[u64],
1105        slice: &ResolvedCaptureSlice,
1106    ) -> Result<(), CaptureError>;
1107    /// Existing evidence-transform cost, excluding original-decision work.
1108    fn capture_usage(
1109        &self,
1110        source: &[u64],
1111        selection: &CaptureSelection,
1112        slice: &ResolvedCaptureSlice,
1113    ) -> Result<CaptureUsage, CaptureError>;
1114    /// Extra original-decision work only: logical native temporaries and additional
1115    /// host materialization. Diagnostic/evidence encoding is charged by the runtime.
1116    fn original_route_usage(
1117        &self,
1118        policy: &InterventionRoutingPolicy,
1119        rows: u64,
1120    ) -> Result<CaptureUsage, CaptureError>;
1121}
1122
1123#[cfg(test)]
1124mod tests;