Skip to main content

deepstrike_core/runtime/kernel/wire/
binding.rs

1//! Binding-safe façade for the canonical durable transition protocol.
2//!
3//! [`CanonicalKernel`] is the public pair that hosts need: the transaction decides whether an
4//! envelope is accepted, while the driver decides what the accepted input means. Keeping the pair
5//! behind one handle makes two invalid call sequences unavailable to bindings:
6//!
7//! * a host cannot call the semantic planner without first preparing a durable record;
8//! * a host cannot commit the transaction without also advancing the driver's committed fold.
9//!
10//! There is intentionally no `step` method. Production callers must prepare, CAS-append the exact
11//! core-produced record bytes, then commit. Checkpoint restore mutates the handle in place so a
12//! binding object keeps its identity across a CAS rebuild.
13
14use super::checkpoint::{
15    CheckpointCandidate, KernelCheckpoint, LogicalKernelState, LogicalStateProjection,
16};
17use super::config::ConfigDefaults;
18use super::driver::{CanonicalOperationDriver, PlannedStep};
19use super::effect::{Digest, KernelEffect};
20use super::envelope::{
21    OperationLifecycle, WireEnvelope, WireRejection, WireRejectionKind, decode_envelope_json,
22};
23use super::fault::{
24    KernelFault, KernelFaultCode, KernelPreparation, PrepareToken, RejectedTransition,
25};
26use super::record::{KernelRecord, RecordPreparation};
27use super::restore::{RestoreCost, restore_operation};
28use super::terminal::KernelTerminal;
29use super::transaction::{
30    CheckpointBoundary, CommittedTransition, DurableHead, InMemoryRecordIndex, KernelTransaction,
31    TailUsage,
32};
33
34type CanonicalTransaction = KernelTransaction<PlannedStep, InMemoryRecordIndex>;
35
36enum DriverRestorePoint {
37    Fresh,
38    Logical(Box<LogicalKernelState>),
39}
40
41/// One canonical operation driven exclusively through the durable transition protocol.
42///
43/// The type is deliberately not `Clone`: duplicating a live candidate would make two handles able
44/// to commit the same prepare token. Rebuild instead uses [`Self::restore`] or
45/// [`Self::restore_bytes`], both of which replace this handle's internals in place.
46pub struct CanonicalKernel {
47    defaults: ConfigDefaults,
48    transaction: CanonicalTransaction,
49    driver: CanonicalOperationDriver,
50    before_candidate: Option<DriverRestorePoint>,
51}
52
53impl std::fmt::Debug for CanonicalKernel {
54    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
55        f.debug_struct("CanonicalKernel")
56            .field("operation_id", &self.transaction.operation_id())
57            .field("head", &self.transaction.head())
58            .field("lifecycle", &self.transaction.lifecycle())
59            .field("has_candidate", &self.transaction.has_candidate())
60            .finish()
61    }
62}
63
64impl Default for CanonicalKernel {
65    fn default() -> Self {
66        Self::new(ConfigDefaults::default())
67    }
68}
69
70impl CanonicalKernel {
71    /// Construct an empty operation under explicit compile-time defaults/bootstrap ceilings.
72    pub fn new(defaults: ConfigDefaults) -> Self {
73        Self {
74            transaction: KernelTransaction::new(defaults.clone(), InMemoryRecordIndex::new()),
75            driver: CanonicalOperationDriver::new(),
76            defaults,
77            before_candidate: None,
78        }
79    }
80
81    /// Strict JSON boundary for dynamic-language bindings.
82    ///
83    /// Decode failures use the same closed `Rejected` arm as typed policy/lifecycle failures; a
84    /// malformed payload never escapes as one language's parser exception.
85    pub fn prepare_json(&mut self, input_json: &str) -> RecordPreparation<PlannedStep> {
86        match decode_envelope_json(input_json, &self.defaults.bootstrap_limits) {
87            Ok(envelope) => self.prepare(&envelope),
88            Err(rejection) => KernelPreparation::Rejected(RejectedTransition {
89                fault: rejection_fault(rejection),
90            }),
91        }
92    }
93
94    /// Plan one typed canonical envelope and stage its core-produced record.
95    pub fn prepare(&mut self, envelope: &WireEnvelope) -> RecordPreparation<PlannedStep> {
96        // Preserve an existing candidate. `KernelTransaction::prepare` will return the structured
97        // transaction-conflict rejection before invoking this closure.
98        if self.before_candidate.is_some() {
99            let Self {
100                transaction,
101                driver,
102                ..
103            } = self;
104            return transaction.prepare(envelope, |context| driver.plan(context));
105        }
106
107        let restore_point = match self.capture_driver_restore_point() {
108            Ok(restore_point) => restore_point,
109            Err(fault) => {
110                return KernelPreparation::Rejected(RejectedTransition { fault });
111            }
112        };
113        let preparation = {
114            let Self {
115                transaction,
116                driver,
117                ..
118            } = self;
119            transaction.prepare(envelope, |context| driver.plan(context))
120        };
121
122        if matches!(preparation, KernelPreparation::Prepared(_)) {
123            self.before_candidate = Some(restore_point);
124            return preparation;
125        }
126
127        // A planner can advance the semantic engine before a later record/tail screen rejects the
128        // preparation. The transaction promises zero mutation for this arm, so restore the driver
129        // to make the promise true for the pair as well.
130        if let Err(fault) = self.restore_driver(restore_point) {
131            return KernelPreparation::Rejected(RejectedTransition { fault });
132        }
133        preparation
134    }
135
136    /// Complete a transition after the host durably appended the candidate record.
137    pub fn commit(
138        &mut self,
139        token: &PrepareToken,
140        appended_head: &Digest,
141    ) -> Result<CommittedTransition<PlannedStep>, KernelFault> {
142        let committed = self.transaction.commit(token, appended_head)?;
143        let step_seq = committed.step_seq;
144        self.before_candidate = None;
145        self.driver.note_committed(step_seq)?;
146        Ok(committed)
147    }
148
149    /// Discard a candidate that did not reach the journal and restore the semantic driver.
150    pub fn abort(&mut self, token: &PrepareToken) -> Result<KernelRecord, KernelFault> {
151        let record = self.transaction.abort(token)?;
152        let restore_point = self.before_candidate.take().ok_or_else(|| {
153            KernelFault::new(
154                KernelFaultCode::TransactionConflict,
155                "the transaction aborted a candidate but the canonical driver has no restore point",
156            )
157        })?;
158        self.restore_driver(restore_point)?;
159        Ok(record)
160    }
161
162    /// Rebuild this exact handle from a verified checkpoint plus records above it.
163    ///
164    /// `checkpoint = None` means the records are the whole retained journal and the fold starts at
165    /// genesis. With a checkpoint, `records` are strictly above `through_step_seq`.
166    pub fn restore(
167        &mut self,
168        checkpoint: Option<&KernelCheckpoint>,
169        records: &[KernelRecord],
170    ) -> Result<RestoreCost, KernelFault> {
171        let restored = restore_operation(
172            checkpoint,
173            records,
174            self.defaults.clone(),
175            InMemoryRecordIndex::from_records(records),
176        )?;
177        let cost = restored.cost;
178        self.transaction = restored.transaction;
179        self.driver = restored.driver;
180        self.before_candidate = None;
181        Ok(cost)
182    }
183
184    /// Binding boundary for native checkpoint/record byte containers.
185    pub fn restore_bytes(
186        &mut self,
187        checkpoint_bytes: Option<&[u8]>,
188        record_bytes: &[Vec<u8>],
189    ) -> Result<RestoreCost, KernelFault> {
190        let checkpoint = checkpoint_bytes
191            .map(KernelCheckpoint::from_checkpoint_bytes)
192            .transpose()
193            .map_err(|error| error.fault())?;
194        let records = record_bytes
195            .iter()
196            .map(|bytes| {
197                KernelRecord::from_record_bytes(bytes)
198                    .map_err(|error| KernelFault::new(error.code(), error.message().to_string()))
199            })
200            .collect::<Result<Vec<_>, _>>()?;
201        self.restore(checkpoint.as_ref(), &records)
202    }
203
204    /// Produce a full-state checkpoint candidate over the current durable head.
205    pub fn checkpoint_candidate(&self) -> Result<CheckpointCandidate, KernelFault> {
206        self.transaction
207            .checkpoint_candidate(self.driver.project_logical_state())
208    }
209
210    /// Produce the incremental checkpoint form over a previously captured logical base.
211    pub fn checkpoint_rebase(
212        &self,
213        base: &KernelCheckpoint,
214    ) -> Result<CheckpointCandidate, KernelFault> {
215        self.transaction
216            .checkpoint_rebase(&base.boundary(), base.logical_state().clone())
217    }
218
219    /// Close the retention boundary after the host durably acknowledged a checkpoint install.
220    pub fn note_checkpoint_acked(
221        &mut self,
222        boundary: &CheckpointBoundary,
223    ) -> Result<TailUsage, KernelFault> {
224        self.transaction.note_checkpoint_acked(boundary)
225    }
226
227    pub fn head(&self) -> Option<DurableHead> {
228        self.transaction.head()
229    }
230
231    pub fn lifecycle(&self) -> OperationLifecycle {
232        self.transaction.lifecycle()
233    }
234
235    pub fn pending_effects(&self) -> impl Iterator<Item = &KernelEffect> {
236        self.transaction.pending_effects()
237    }
238
239    pub fn terminal(&self) -> Option<&KernelTerminal> {
240        self.transaction.terminal()
241    }
242
243    /// Return the live kernel-issued attempt for a child task, including after checkpoint restore.
244    pub fn attempt_id(&self, task_id: &str) -> Option<super::scalar::AttemptId> {
245        self.driver.attempt_id(task_id).cloned()
246    }
247
248    /// Current scheduler turn, restored from the canonical journal/checkpoint state.
249    pub fn turn(&self) -> u32 {
250        self.driver.engine().map_or(0, |engine| engine.turn)
251    }
252
253    /// Recovery replay budget in bytes, when the operation has been configured.
254    pub fn recovery_content_bytes(&self) -> Option<usize> {
255        self.driver.engine().map(|engine| {
256            let tokens = engine
257                .ctx
258                .config
259                .recovery_content_tokens(engine.ctx.max_tokens);
260            engine.ctx.engine.token_budget_to_bytes(tokens)
261        })
262    }
263
264    /// Task-state references that context pressure must keep resident.
265    pub fn preserved_refs(&self) -> Vec<String> {
266        self.driver
267            .engine()
268            .map(|engine| engine.ctx.partitions.task_state.preserved_refs.clone())
269            .unwrap_or_default()
270    }
271
272    /// Count text using the configured canonical context token engine.
273    pub fn count_tokens(&self, text: &str) -> Option<u32> {
274        self.driver
275            .engine()
276            .map(|engine| engine.ctx.engine.count(text))
277    }
278
279    /// Cumulative kernel-owned child spawn count for this operation.
280    pub fn local_subagents_spawned(&self) -> u32 {
281        self.driver
282            .engine()
283            .map_or(0, |engine| engine.local_subagents_spawned())
284    }
285
286    /// Messages added to the canonical operation history.
287    pub fn new_messages(&self) -> Vec<crate::types::message::Message> {
288        self.driver
289            .engine()
290            .map(|engine| engine.drain_new_messages())
291            .unwrap_or_default()
292    }
293
294    pub fn poison(&self) -> Option<&KernelFault> {
295        self.transaction.poison().or_else(|| self.driver.poison())
296    }
297
298    fn capture_driver_restore_point(&self) -> Result<DriverRestorePoint, KernelFault> {
299        if self.transaction.config().is_none() {
300            return Ok(DriverRestorePoint::Fresh);
301        }
302        let LogicalStateProjection {
303            root_kind,
304            focus,
305            syscall,
306            scheduler,
307            context_vm,
308        } = self.driver.project_logical_state();
309        let transition = self
310            .transaction
311            .transition_state_for_restore(root_kind, focus)?;
312        Ok(DriverRestorePoint::Logical(Box::new(LogicalKernelState {
313            transition,
314            syscall,
315            scheduler,
316            context_vm,
317        })))
318    }
319
320    fn restore_driver(&mut self, restore_point: DriverRestorePoint) -> Result<(), KernelFault> {
321        self.driver = match restore_point {
322            DriverRestorePoint::Fresh => CanonicalOperationDriver::new(),
323            DriverRestorePoint::Logical(state) => CanonicalOperationDriver::restore_logical_state(
324                &state.transition.resolved_config,
325                &state,
326            )?,
327        };
328        Ok(())
329    }
330}
331
332fn rejection_fault(rejection: WireRejection) -> KernelFault {
333    let code = match rejection.kind {
334        WireRejectionKind::PolicyViolation => KernelFaultCode::InvalidConfig,
335        _ => KernelFaultCode::MalformedEnvelope,
336    };
337    KernelFault::new(code, rejection.message)
338}
339
340#[cfg(test)]
341mod tests {
342    use serde_json::Value;
343
344    use super::CanonicalKernel;
345    use crate::runtime::kernel::wire::{
346        KernelFaultCode, KernelPreparation, PrepareToken, WireEnvelope,
347    };
348
349    fn golden_agent_root() -> Value {
350        serde_json::from_str(include_str!(
351            "../../../../../../tests/fixtures/kernel-wire/golden_lifecycle_agent_root.json"
352        ))
353        .expect("golden fixture")
354    }
355
356    fn commit_input(kernel: &mut CanonicalKernel, input: &str) {
357        let prepared = kernel.prepare_json(input);
358        let KernelPreparation::Prepared(prepared) = prepared else {
359            panic!("expected prepared transition");
360        };
361        kernel
362            .commit(&prepared.token, prepared.record.record_digest())
363            .expect("commit");
364    }
365
366    #[test]
367    fn canonical_kernel_produces_the_shared_genesis_record() {
368        let fixture = golden_agent_root();
369        let mut kernel = CanonicalKernel::default();
370        let preparation = kernel.prepare_json(&fixture["links"][0]["envelope"].to_string());
371        let KernelPreparation::Prepared(prepared) = preparation else {
372            panic!("golden envelope must prepare");
373        };
374
375        assert_eq!(
376            prepared.record.record_digest().as_str(),
377            fixture["genesis_digest"].as_str().unwrap()
378        );
379        assert_eq!(
380            std::str::from_utf8(prepared.record.record_bytes().as_slice()).unwrap(),
381            serde_json::to_string(&fixture["links"][0]["record"]).unwrap()
382        );
383    }
384
385    #[test]
386    fn abort_restores_the_driver_before_the_next_prepare() {
387        let fixture = golden_agent_root();
388        let mut kernel = CanonicalKernel::default();
389        commit_input(&mut kernel, &fixture["links"][0]["envelope"].to_string());
390
391        let start = fixture["links"][1]["envelope"].clone();
392        let first = kernel.prepare_json(&start.to_string());
393        let KernelPreparation::Prepared(first) = first else {
394            panic!("start must prepare");
395        };
396        let first_digest = first.record.record_digest().clone();
397        kernel.abort(&first.token).expect("abort before append");
398
399        let second = kernel.prepare_json(&start.to_string());
400        let KernelPreparation::Prepared(second) = second else {
401            panic!("the same input must prepare after abort");
402        };
403        assert_eq!(second.record.record_digest(), &first_digest);
404    }
405
406    #[test]
407    fn malformed_and_unknown_envelopes_are_structured_rejections() {
408        let mut kernel = CanonicalKernel::default();
409        let malformed = kernel.prepare_json("{");
410        assert_eq!(
411            malformed.fault().map(|fault| fault.code),
412            Some(KernelFaultCode::MalformedEnvelope)
413        );
414
415        let fixture = golden_agent_root();
416        let mut unknown = fixture["links"][0]["envelope"].clone();
417        unknown
418            .as_object_mut()
419            .unwrap()
420            .insert("session_id".to_string(), Value::String("host-only".into()));
421        let rejected = kernel.prepare_json(&unknown.to_string());
422        assert_eq!(
423            rejected.fault().map(|fault| fault.code),
424            Some(KernelFaultCode::MalformedEnvelope)
425        );
426    }
427
428    #[test]
429    fn restore_replaces_the_same_typed_handle() {
430        let fixture = golden_agent_root();
431        let mut kernel = CanonicalKernel::default();
432        commit_input(&mut kernel, &fixture["links"][0]["envelope"].to_string());
433        let checkpoint = kernel
434            .checkpoint_candidate()
435            .expect("configured operation checkpoints")
436            .decode()
437            .expect("checkpoint verifies");
438
439        let start: WireEnvelope =
440            serde_json::from_value(fixture["links"][1]["envelope"].clone()).unwrap();
441        let KernelPreparation::Prepared(prepared) = kernel.prepare(&start) else {
442            panic!("start prepares");
443        };
444        let post_checkpoint_record = prepared.record.clone();
445        let expected_head = prepared.record.record_digest().clone();
446        kernel
447            .commit(&prepared.token, prepared.record.record_digest())
448            .unwrap();
449
450        let handle_address = std::ptr::addr_of!(kernel);
451        kernel
452            .restore(Some(&checkpoint), &[post_checkpoint_record])
453            .expect("restore");
454        assert_eq!(std::ptr::addr_of!(kernel), handle_address);
455        assert_eq!(kernel.head().unwrap().digest, expected_head);
456
457        let no_candidate = PrepareToken::new("no-candidate").unwrap();
458        assert!(kernel.abort(&no_candidate).is_err());
459    }
460}