1use super::*;
2use std::collections::{HashMap, HashSet, VecDeque};
3use std::ops::{Deref, DerefMut};
4
5pub(super) const EVENT_REPLAY_WINDOW_CAPACITY: usize = 256;
6const COMPLETED_EFFECT_REPLAY_WINDOW_CAPACITY: usize = 256;
7const SNAPSHOT_INPUT_LIMIT: usize = 10_000;
8const MAX_INPUT_BYTES: usize = 16 * 1024 * 1024;
9const SNAPSHOT_JOURNAL_BYTES_LIMIT: usize = 64 * 1024 * 1024;
10
11#[derive(Clone)]
12struct RecordedTransition {
13 fingerprint: Vec<u8>,
14 step: KernelStep,
15}
16
17#[derive(serde::Serialize)]
18struct KernelSnapshotRefV2<'a> {
19 snapshot_version: u32,
20 abi_version: u32,
21 initial_policy: KernelSnapshotPolicy,
22 lifecycle: KernelLifecycle,
23 #[serde(skip_serializing_if = "Option::is_none")]
24 operation_id: Option<&'a str>,
25 next_step_seq: u64,
26 snapshot_input_limit: usize,
27 max_input_bytes: usize,
28 snapshot_journal_bytes_limit: usize,
29 accepted_input_bytes: usize,
30 accepted_inputs: &'a [KernelInput],
31 #[serde(skip_serializing_if = "Option::is_none")]
32 last_step: Option<&'a KernelStep>,
33}
34
35#[derive(Clone)]
36struct AcceptedCancellation {
37 reason: CancellationReason,
38 pending_call_ids: Vec<String>,
39 step: KernelStep,
40}
41
42struct ReplayWindow {
43 entries: HashMap<String, RecordedTransition>,
44 order: VecDeque<String>,
45 capacity: usize,
46}
47
48impl ReplayWindow {
49 fn new(capacity: usize) -> Self {
50 Self {
51 entries: HashMap::with_capacity(capacity),
52 order: VecDeque::with_capacity(capacity),
53 capacity,
54 }
55 }
56
57 fn get(&self, identity: &str) -> Option<&RecordedTransition> {
58 self.entries.get(identity)
59 }
60
61 fn insert(&mut self, identity: String, transition: RecordedTransition) {
62 if self.entries.len() == self.capacity {
63 if let Some(expired_identity) = self.order.pop_front() {
64 self.entries.remove(&expired_identity);
65 }
66 }
67 self.order.push_back(identity.clone());
68 self.entries.insert(identity, transition);
69 }
70
71 fn set_capacity(&mut self, capacity: usize) {
72 self.capacity = capacity;
73 while self.entries.len() > capacity {
74 if let Some(expired_identity) = self.order.pop_front() {
75 self.entries.remove(&expired_identity);
76 }
77 }
78 self.entries.shrink_to(capacity);
79 self.order.shrink_to(capacity);
80 }
81
82 #[cfg(test)]
83 fn len(&self) -> usize {
84 self.entries.len()
85 }
86}
87
88#[derive(Debug, Clone, Copy, PartialEq, Eq)]
89enum PendingEffectKind {
90 Provider,
91 Tool,
92 Milestone,
93 Approval,
94 WorkflowSpawn,
95 Preempt,
96 MemoryPersist,
97 MemoryQuery,
98 LargeResultSpool,
99 PageOutArchive,
100}
101
102#[derive(Clone, Copy)]
103enum LifecycleTransition {
104 Stay,
105 Configure,
106 Start,
107 Resume,
108}
109
110#[derive(Clone, Copy, PartialEq, Eq)]
111enum StepMode {
112 Live,
113 SnapshotReplay,
114}
115
116struct StepIdentity {
117 operation_id: String,
118 input_event_id: String,
119 step_seq: u64,
120}
121
122impl StepIdentity {
123 fn empty(&self, observations: Vec<KernelObservation>) -> KernelStep {
124 KernelStep::empty(
125 self.operation_id.clone(),
126 self.input_event_id.clone(),
127 self.step_seq,
128 observations,
129 )
130 }
131
132 fn single(self, action: LoopAction, observations: Vec<KernelObservation>) -> KernelStep {
133 KernelStep::single(
134 self.operation_id,
135 self.input_event_id,
136 self.step_seq,
137 action,
138 observations,
139 )
140 }
141}
142
143#[doc(hidden)]
144pub struct KernelRuntimeState {
145 sm: LoopStateMachine,
146 initial_policy: SchedulerBudget,
147 lifecycle: KernelLifecycle,
148 operation_id: Option<String>,
149 next_step_seq: u64,
150 recorded_events: ReplayWindow,
151 pending_effects: HashMap<String, PendingEffectKind>,
152 completed_effects: ReplayWindow,
153 memory_records: crate::mm::memory::MemoryRecordStore,
154 pending_memory_write: Option<crate::mm::memory::MemoryRecord>,
155 pending_memory_store: Option<crate::mm::memory::MemoryRecordStore>,
156 pending_memory_query: Option<(crate::mm::memory::MemoryQuery, usize)>,
157 budget_usage_reported: bool,
158 accepted_cancellation: Option<AcceptedCancellation>,
159 accepted_inputs: Vec<KernelInput>,
160 accepted_input_count: usize,
161 accepted_input_bytes: usize,
162 snapshot_input_limit: usize,
163 max_input_bytes: usize,
164 snapshot_journal_bytes_limit: usize,
165 snapshot_overflowed: bool,
166 last_step: Option<KernelStep>,
167}
168
169struct PreparedCandidate {
170 token: String,
171 base_generation: u64,
172 state: Box<KernelRuntimeState>,
173 step: KernelStep,
174 accepted_inputs_before: usize,
175}
176
177pub struct KernelRuntime {
180 state: Option<Box<KernelRuntimeState>>,
181 prepared: Option<PreparedCandidate>,
182 generation: u64,
183 poisoned: bool,
184 next_prepare_token: u64,
185}
186
187impl Deref for KernelRuntime {
188 type Target = KernelRuntimeState;
189
190 fn deref(&self) -> &Self::Target {
191 self.state
192 .as_deref()
193 .expect("kernel runtime state is staged; commit or abort the prepared transition")
194 }
195}
196
197impl DerefMut for KernelRuntime {
198 fn deref_mut(&mut self) -> &mut Self::Target {
199 self.state
200 .as_deref_mut()
201 .expect("kernel runtime state is staged; commit or abort the prepared transition")
202 }
203}
204
205impl KernelRuntime {
206 pub fn new(policy: SchedulerBudget) -> Self {
207 Self {
208 state: Some(Box::new(KernelRuntimeState {
209 sm: LoopStateMachine::new(policy.clone()),
210 initial_policy: policy,
211 lifecycle: KernelLifecycle::Created,
212 operation_id: None,
213 next_step_seq: 1,
214 recorded_events: ReplayWindow::new(EVENT_REPLAY_WINDOW_CAPACITY),
215 pending_effects: HashMap::new(),
216 completed_effects: ReplayWindow::new(COMPLETED_EFFECT_REPLAY_WINDOW_CAPACITY),
217 memory_records: crate::mm::memory::MemoryRecordStore::default(),
218 pending_memory_write: None,
219 pending_memory_store: None,
220 pending_memory_query: None,
221 budget_usage_reported: false,
222 accepted_cancellation: None,
223 accepted_inputs: Vec::new(),
224 accepted_input_count: 0,
225 accepted_input_bytes: 0,
226 snapshot_input_limit: SNAPSHOT_INPUT_LIMIT,
227 max_input_bytes: MAX_INPUT_BYTES,
228 snapshot_journal_bytes_limit: SNAPSHOT_JOURNAL_BYTES_LIMIT,
229 snapshot_overflowed: false,
230 last_step: None,
231 })),
232 prepared: None,
233 generation: 0,
234 poisoned: false,
235 next_prepare_token: 1,
236 }
237 }
238
239 pub fn snapshot(&self) -> Result<KernelSnapshot, KernelFault> {
241 self.ensure_snapshot_available()?;
242 Ok(KernelSnapshot {
243 snapshot_version: KERNEL_SNAPSHOT_VERSION,
244 abi_version: KERNEL_ABI_VERSION,
245 initial_policy: KernelSnapshotPolicy::from(&self.initial_policy),
246 lifecycle: self.lifecycle,
247 operation_id: self.operation_id.clone(),
248 next_step_seq: self.next_step_seq,
249 snapshot_input_limit: self.snapshot_input_limit,
250 max_input_bytes: self.max_input_bytes,
251 snapshot_journal_bytes_limit: self.snapshot_journal_bytes_limit,
252 accepted_input_bytes: self.accepted_input_bytes,
253 accepted_inputs: self.accepted_inputs.clone(),
254 last_step: self.last_step.clone(),
255 })
256 }
257
258 pub fn snapshot_json(&self) -> Result<String, KernelFault> {
259 self.ensure_snapshot_available()?;
260 let snapshot = KernelSnapshotRefV2 {
261 snapshot_version: KERNEL_SNAPSHOT_VERSION,
262 abi_version: KERNEL_ABI_VERSION,
263 initial_policy: KernelSnapshotPolicy::from(&self.initial_policy),
264 lifecycle: self.lifecycle,
265 operation_id: self.operation_id.as_deref(),
266 next_step_seq: self.next_step_seq,
267 snapshot_input_limit: self.snapshot_input_limit,
268 max_input_bytes: self.max_input_bytes,
269 snapshot_journal_bytes_limit: self.snapshot_journal_bytes_limit,
270 accepted_input_bytes: self.accepted_input_bytes,
271 accepted_inputs: &self.accepted_inputs,
272 last_step: self.last_step.as_ref(),
273 };
274 serde_json::to_string(&snapshot).map_err(|error| {
275 snapshot_fault(
276 self.operation_id.clone(),
277 format!("failed to encode kernel snapshot: {error}"),
278 )
279 })
280 }
281
282 pub fn restore_snapshot(snapshot: KernelSnapshot) -> Result<Self, KernelFault> {
285 if snapshot.snapshot_version != KERNEL_SNAPSHOT_VERSION
286 || snapshot.abi_version != KERNEL_ABI_VERSION
287 || !(1..=100_000).contains(&snapshot.snapshot_input_limit)
288 || !(256..=64 * 1024 * 1024).contains(&snapshot.max_input_bytes)
289 || !(256..=1024 * 1024 * 1024usize).contains(&snapshot.snapshot_journal_bytes_limit)
290 || snapshot.accepted_inputs.len() > snapshot.snapshot_input_limit
291 || snapshot.accepted_input_bytes > snapshot.snapshot_journal_bytes_limit
292 {
293 return Err(snapshot_fault(
294 snapshot.operation_id,
295 "incompatible kernel snapshot version or bounds".to_string(),
296 ));
297 }
298
299 let initial_policy = SchedulerBudget::try_from(&snapshot.initial_policy)
300 .map_err(|message| snapshot_fault(snapshot.operation_id.clone(), message))?;
301 if KernelSnapshotPolicy::from(&initial_policy) != snapshot.initial_policy {
302 return Err(snapshot_fault(
303 snapshot.operation_id,
304 "kernel snapshot policy is not canonically encoded".to_string(),
305 ));
306 }
307 let mut runtime = Self::new(initial_policy);
308 runtime.snapshot_input_limit = snapshot.snapshot_input_limit;
309 runtime.max_input_bytes = snapshot.max_input_bytes;
310 runtime.snapshot_journal_bytes_limit = snapshot.snapshot_journal_bytes_limit;
311 let mut event_ids = HashSet::with_capacity(snapshot.accepted_inputs.len());
312 for input in snapshot.accepted_inputs.iter().cloned() {
313 if !event_ids.insert(input.event_id.clone()) {
314 return Err(snapshot_fault(
315 snapshot.operation_id.clone(),
316 format!(
317 "kernel snapshot journal repeats accepted event_id {}",
318 input.event_id
319 ),
320 ));
321 }
322 let step = runtime.step_internal(input, StepMode::SnapshotReplay);
323 if let Some(fault) = step.faults.first() {
324 return Err(snapshot_fault(
325 snapshot.operation_id.clone(),
326 format!(
327 "snapshot replay rejected an accepted input: {}",
328 fault.message
329 ),
330 ));
331 }
332 }
333 let rebuilt_last = serde_json::to_value(&runtime.last_step).ok();
334 let expected_last = serde_json::to_value(&snapshot.last_step).ok();
335 if runtime.lifecycle != snapshot.lifecycle
336 || runtime.operation_id != snapshot.operation_id
337 || runtime.next_step_seq != snapshot.next_step_seq
338 || runtime.snapshot_input_limit != snapshot.snapshot_input_limit
339 || runtime.max_input_bytes != snapshot.max_input_bytes
340 || runtime.snapshot_journal_bytes_limit != snapshot.snapshot_journal_bytes_limit
341 || runtime.accepted_input_count != snapshot.accepted_inputs.len()
342 || runtime.accepted_input_bytes != snapshot.accepted_input_bytes
343 || runtime.snapshot_overflowed
344 || rebuilt_last != expected_last
345 {
346 return Err(snapshot_fault(
347 snapshot.operation_id,
348 "kernel snapshot metadata does not match deterministic replay".to_string(),
349 ));
350 }
351 runtime.accepted_inputs = snapshot.accepted_inputs;
352 runtime.generation = runtime.accepted_input_count as u64;
353 Ok(runtime)
354 }
355
356 pub fn restore_snapshot_json(snapshot_json: &str) -> Result<Self, KernelFault> {
357 let snapshot = serde_json::from_str(snapshot_json).map_err(|error| {
358 snapshot_fault(None, format!("invalid kernel snapshot JSON: {error}"))
359 })?;
360 Self::restore_snapshot(snapshot)
361 }
362
363 #[cfg(test)]
364 pub(super) fn state_machine(&self) -> &LoopStateMachine {
365 &self.sm
366 }
367
368 #[cfg(test)]
369 pub(super) fn clear_test_observations(&mut self) {
370 self.sm.take_observations();
371 }
372
373 #[cfg(test)]
374 pub(super) fn push_test_history(&mut self, message: Message, tokens: u32) {
375 self.sm.ctx.push_history(message, tokens);
376 }
377
378 #[cfg(test)]
379 pub(super) fn accepted_snapshot_input_count(&self) -> usize {
380 self.accepted_inputs.len()
381 }
382
383 pub fn is_terminal(&self) -> bool {
384 self.lifecycle.is_terminal()
385 }
386
387 pub fn diagnostics(&self) -> KernelDiagnostics {
388 KernelDiagnostics {
389 lifecycle: self.lifecycle,
390 next_step_seq: self.next_step_seq,
391 accepted_input_count: self.accepted_input_count,
392 accepted_input_bytes: self.accepted_input_bytes,
393 snapshot_input_limit: self.snapshot_input_limit,
394 snapshot_journal_bytes_limit: self.snapshot_journal_bytes_limit,
395 max_input_bytes: self.max_input_bytes,
396 snapshot_overflowed: self.snapshot_overflowed,
397 recorded_event_count: self.recorded_events.entries.len(),
398 completed_effect_count: self.completed_effects.entries.len(),
399 pending_effect_count: self.pending_effects.len(),
400 }
401 }
402
403 pub fn lifecycle(&self) -> KernelLifecycle {
404 self.lifecycle
405 }
406
407 pub fn turn(&self) -> u32 {
408 self.sm.turn
409 }
410
411 pub fn recovery_content_bytes(&self) -> usize {
412 let tokens = self
413 .sm
414 .ctx
415 .config
416 .recovery_content_tokens(self.sm.ctx.max_tokens);
417 self.sm.ctx.engine.token_budget_to_bytes(tokens)
418 }
419
420 pub fn render(&self) -> RenderedContext {
421 self.sm.ctx.render()
422 }
423
424 pub fn drain_new_messages(&mut self) -> Vec<Message> {
425 self.sm.drain_new_messages()
426 }
427
428 pub fn preserved_refs(&self) -> Vec<String> {
429 self.sm.ctx.partitions.task_state.preserved_refs.clone()
430 }
431
432 pub fn count_tokens(&self, text: &str) -> u32 {
433 self.sm.ctx.engine.count(text)
434 }
435
436 pub fn local_subagents_spawned(&self) -> u32 {
439 self.sm.local_subagents_spawned()
440 }
441
442 #[cfg(test)]
443 pub(super) fn pending_provider_effect_id(&self) -> String {
444 self.pending_effect_id(PendingEffectKind::Provider)
445 }
446
447 #[cfg(test)]
448 pub(super) fn pending_tool_effect_id(&self) -> String {
449 self.pending_effect_id(PendingEffectKind::Tool)
450 }
451
452 #[cfg(test)]
453 fn pending_effect_id(&self, kind: PendingEffectKind) -> String {
454 let matches = self
455 .pending_effects
456 .iter()
457 .filter(|(_, pending_kind)| **pending_kind == kind)
458 .collect::<Vec<_>>();
459 assert_eq!(
460 matches.len(),
461 1,
462 "test transition must have one {kind:?} effect"
463 );
464 matches[0].0.clone()
465 }
466
467 #[cfg(test)]
468 pub(super) fn recorded_event_count(&self) -> usize {
469 self.recorded_events.len()
470 }
471
472 fn boundary_max_input_bytes(&self) -> usize {
473 self.state
474 .as_deref()
475 .or_else(|| {
476 self.prepared
477 .as_ref()
478 .map(|candidate| candidate.state.as_ref())
479 })
480 .map_or(MAX_INPUT_BYTES, |state| state.max_input_bytes)
481 }
482
483 fn boundary_step_seq(&self) -> u64 {
484 self.state
485 .as_deref()
486 .map(|state| state.next_step_seq)
487 .or_else(|| {
488 self.prepared
489 .as_ref()
490 .map(|candidate| candidate.step.step_seq)
491 })
492 .unwrap_or(1)
493 }
494
495 pub fn prepare_step(&mut self, mut input: KernelInput) -> KernelPreparedStep {
499 normalize_input(&mut input);
500 let base_generation = self.generation;
501 let operation_id = input.operation_id.clone();
502 let event_id = input.event_id.clone();
503
504 if self.poisoned {
505 return KernelPreparedStep {
506 status: KernelPreparationStatus::Rejected,
507 base_generation,
508 prepare_token: None,
509 input,
510 step: transaction_fault_step(
511 operation_id,
512 event_id,
513 self.boundary_step_seq(),
514 KernelFaultCode::TransactionConflict,
515 "kernel runtime was invalidated by a transaction consistency error".to_string(),
516 ),
517 };
518 }
519
520 if let Some(candidate) = &self.prepared {
521 return KernelPreparedStep {
522 status: KernelPreparationStatus::Rejected,
523 base_generation,
524 prepare_token: None,
525 input,
526 step: transaction_fault_step(
527 operation_id,
528 event_id,
529 candidate.step.step_seq,
530 KernelFaultCode::TransactionConflict,
531 "another kernel transition is already prepared".to_string(),
532 ),
533 };
534 }
535
536 if self.snapshot_overflowed {
537 return KernelPreparedStep {
538 status: KernelPreparationStatus::Rejected,
539 base_generation,
540 prepare_token: None,
541 input,
542 step: transaction_fault_step(
543 operation_id,
544 event_id,
545 self.next_step_seq,
546 KernelFaultCode::ResourceLimitExceeded,
547 "kernel journal cannot roll back a prepared transition after overflow"
548 .to_string(),
549 ),
550 };
551 }
552
553 let fingerprint = serde_json::to_vec(&input)
554 .expect("KernelInput serialization must succeed after typed construction");
555 let was_exact_replay = self
556 .recorded_events
557 .get(&event_id)
558 .is_some_and(|recorded| recorded.fingerprint == fingerprint);
559 let accepted_inputs_before = self.accepted_inputs.len();
560 let step = self.step_internal(input.clone(), StepMode::Live);
561 let candidate_state = self
562 .state
563 .take()
564 .expect("step execution must leave a runtime state");
565
566 if !step.faults.is_empty() {
567 self.state = Some(candidate_state);
568 return KernelPreparedStep {
569 status: KernelPreparationStatus::Rejected,
570 base_generation,
571 prepare_token: None,
572 input,
573 step,
574 };
575 }
576
577 if was_exact_replay {
578 self.state = Some(candidate_state);
579 return KernelPreparedStep {
580 status: KernelPreparationStatus::Replayed,
581 base_generation,
582 prepare_token: None,
583 input,
584 step,
585 };
586 }
587
588 let token = format!("kernel-prepare-{}", self.next_prepare_token);
589 self.next_prepare_token = self.next_prepare_token.saturating_add(1);
590 self.prepared = Some(PreparedCandidate {
591 token: token.clone(),
592 base_generation,
593 state: candidate_state,
594 step: step.clone(),
595 accepted_inputs_before,
596 });
597 KernelPreparedStep {
598 status: KernelPreparationStatus::Prepared,
599 base_generation,
600 prepare_token: Some(token),
601 input,
602 step,
603 }
604 }
605
606 pub fn commit_prepared(&mut self, token: &str) -> Result<KernelStep, KernelFault> {
609 if self.poisoned {
610 return Err(transaction_conflict_fault(
611 None,
612 "kernel runtime was invalidated by a transaction consistency error".to_string(),
613 ));
614 }
615 let Some(candidate) = self.prepared.as_ref() else {
616 return Err(transaction_conflict_fault(
617 None,
618 "no kernel transition is prepared".to_string(),
619 ));
620 };
621 if candidate.token != token {
622 let operation_id = candidate.state.operation_id.clone();
623 self.poisoned = true;
624 return Err(transaction_conflict_fault(
625 operation_id,
626 "prepare token does not match the staged transition".to_string(),
627 ));
628 }
629 if candidate.base_generation != self.generation {
630 let operation_id = candidate.state.operation_id.clone();
631 self.poisoned = true;
632 return Err(transaction_conflict_fault(
633 operation_id,
634 "prepared transition no longer matches the runtime generation".to_string(),
635 ));
636 }
637
638 let candidate = self
639 .prepared
640 .take()
641 .expect("prepared candidate was validated above");
642 let step = candidate.step.clone();
643 self.state = Some(candidate.state);
644 self.generation = self.generation.saturating_add(1);
645 Ok(step)
646 }
647
648 pub fn abort_prepared(&mut self, token: &str) -> Result<(), KernelFault> {
652 if self.poisoned {
653 return Err(transaction_conflict_fault(
654 None,
655 "kernel runtime was invalidated by a transaction consistency error".to_string(),
656 ));
657 }
658 let Some(candidate) = self.prepared.as_ref() else {
659 return Err(transaction_conflict_fault(
660 None,
661 "no kernel transition is prepared".to_string(),
662 ));
663 };
664 if candidate.token != token {
665 let operation_id = candidate.state.operation_id.clone();
666 self.poisoned = true;
667 return Err(transaction_conflict_fault(
668 operation_id,
669 "prepare token does not match the staged transition".to_string(),
670 ));
671 }
672
673 let candidate = self
674 .prepared
675 .take()
676 .expect("prepared candidate was validated above");
677 let mut committed_inputs = candidate.state.accepted_inputs.clone();
678 committed_inputs.truncate(candidate.accepted_inputs_before);
679 match Self::rebuild_accepted_inputs(
680 candidate.state.initial_policy.clone(),
681 committed_inputs,
682 ) {
683 Ok(mut restored) => {
684 self.state = restored.state.take();
685 Ok(())
686 }
687 Err(fault) => {
688 self.prepared = Some(candidate);
689 Err(fault)
690 }
691 }
692 }
693
694 fn rebuild_accepted_inputs(
695 initial_policy: SchedulerBudget,
696 accepted_inputs: Vec<KernelInput>,
697 ) -> Result<Self, KernelFault> {
698 let mut runtime = Self::new(initial_policy);
699 for input in accepted_inputs.iter().cloned() {
700 let step = runtime.step_internal(input, StepMode::SnapshotReplay);
701 if let Some(fault) = step.faults.first() {
702 return Err(snapshot_fault(
703 runtime.operation_id.clone(),
704 format!(
705 "committed transaction replay rejected an accepted input: {}",
706 fault.message
707 ),
708 ));
709 }
710 }
711 runtime.accepted_inputs = accepted_inputs;
712 runtime.generation = runtime.accepted_input_count as u64;
713 Ok(runtime)
714 }
715
716 pub fn step_json(&mut self, input_json: &str) -> Result<KernelStep, serde_json::Error> {
720 let max_input_bytes = self.boundary_max_input_bytes();
721 if input_json.len() > max_input_bytes {
722 return Ok(transaction_fault_step(
723 String::new(),
724 String::new(),
725 self.boundary_step_seq(),
726 KernelFaultCode::ResourceLimitExceeded,
727 format!(
728 "kernel input is {} bytes; configured maximum is {} bytes",
729 input_json.len(),
730 max_input_bytes
731 ),
732 ));
733 }
734 let value: serde_json::Value = serde_json::from_str(input_json)?;
735 if value.get("version").and_then(serde_json::Value::as_u64)
736 != Some(KERNEL_ABI_VERSION as u64)
737 {
738 let operation_id = value
739 .get("operation_id")
740 .and_then(serde_json::Value::as_str)
741 .unwrap_or_default()
742 .to_string();
743 let event_id = value
744 .get("event_id")
745 .and_then(serde_json::Value::as_str)
746 .unwrap_or_default()
747 .to_string();
748 let received_version = value
749 .get("version")
750 .and_then(serde_json::Value::as_u64)
751 .map_or_else(|| "missing".to_string(), |version| version.to_string());
752 return Ok(transaction_fault_step(
753 operation_id,
754 event_id,
755 self.boundary_step_seq(),
756 KernelFaultCode::VersionMismatch,
757 format!(
758 "kernel ABI version mismatch: input v{received_version}, kernel v{KERNEL_ABI_VERSION}"
759 ),
760 ));
761 }
762
763 serde_json::from_value(value).map(|input| self.step(input))
764 }
765
766 pub fn prepare_step_json(
768 &mut self,
769 input_json: &str,
770 ) -> Result<KernelPreparedStep, serde_json::Error> {
771 let value: serde_json::Value = serde_json::from_str(input_json)?;
772 let input: KernelInput = serde_json::from_value(value)?;
773 let max_input_bytes = self.boundary_max_input_bytes();
774 if input_json.len() > max_input_bytes {
775 return Ok(KernelPreparedStep {
776 status: KernelPreparationStatus::Rejected,
777 base_generation: self.generation,
778 prepare_token: None,
779 step: transaction_fault_step(
780 input.operation_id.clone(),
781 input.event_id.clone(),
782 self.boundary_step_seq(),
783 KernelFaultCode::ResourceLimitExceeded,
784 format!(
785 "kernel input is {} bytes; configured maximum is {} bytes",
786 input_json.len(),
787 max_input_bytes
788 ),
789 ),
790 input,
791 });
792 }
793 Ok(self.prepare_step(input))
794 }
795
796 pub fn step(&mut self, input: KernelInput) -> KernelStep {
797 if self.poisoned {
798 return transaction_fault_step(
799 input.operation_id,
800 input.event_id,
801 self.boundary_step_seq(),
802 KernelFaultCode::TransactionConflict,
803 "kernel runtime was invalidated by a transaction consistency error".to_string(),
804 );
805 }
806 if let Some(candidate) = &self.prepared {
807 return transaction_fault_step(
808 input.operation_id,
809 input.event_id,
810 candidate.step.step_seq,
811 KernelFaultCode::TransactionConflict,
812 "another kernel transition is already prepared".to_string(),
813 );
814 }
815 let accepted_input_count = self.accepted_input_count;
816 let step = self.step_internal(input, StepMode::Live);
817 if self.accepted_input_count > accepted_input_count {
818 self.generation = self.generation.saturating_add(1);
819 }
820 step
821 }
822
823 fn step_internal(&mut self, mut input: KernelInput, mode: StepMode) -> KernelStep {
824 normalize_input(&mut input);
825 let operation_id = input.operation_id.clone();
826 let event_id = input.event_id.clone();
827
828 if input.version != KERNEL_ABI_VERSION {
829 return self.fault_step(
830 operation_id,
831 event_id,
832 KernelFaultCode::VersionMismatch,
833 format!(
834 "kernel ABI version mismatch: input v{}, kernel v{}",
835 input.version, KERNEL_ABI_VERSION
836 ),
837 None,
838 );
839 }
840
841 if operation_id.is_empty() || event_id.is_empty() {
842 return self.fault_step(
843 operation_id,
844 event_id,
845 KernelFaultCode::InvalidConfig,
846 "operation_id and event_id must be non-empty".to_string(),
847 None,
848 );
849 }
850
851 let fingerprint = serde_json::to_vec(&input)
852 .expect("KernelInput serialization must succeed after typed construction");
853 let input_bytes = fingerprint.len();
854 if mode == StepMode::Live && input_bytes > self.max_input_bytes {
855 return self.fault_step(
856 operation_id,
857 event_id,
858 KernelFaultCode::ResourceLimitExceeded,
859 format!(
860 "kernel input is {input_bytes} bytes; configured maximum is {} bytes",
861 self.max_input_bytes
862 ),
863 None,
864 );
865 }
866 if let Some(recorded) = self.recorded_events.get(&event_id) {
867 if recorded.fingerprint == fingerprint {
868 return recorded.step.clone();
869 }
870 return self.fault_step(
871 operation_id,
872 event_id,
873 KernelFaultCode::DuplicateEventConflict,
874 "event_id was already accepted with a different payload".to_string(),
875 None,
876 );
877 }
878
879 if let Some(bound_operation_id) = &self.operation_id {
880 if bound_operation_id != &operation_id {
881 return self.fault_step(
882 operation_id,
883 event_id,
884 KernelFaultCode::OperationMismatch,
885 format!("input operation does not match bound operation {bound_operation_id}"),
886 None,
887 );
888 }
889 }
890
891 if let KernelInputEvent::CancelOperation {
892 operation_id: cancelled_operation_id,
893 reason,
894 pending_call_ids,
895 } = &input.event
896 {
897 if cancelled_operation_id != &operation_id {
898 return self.fault_step(
899 operation_id,
900 event_id,
901 KernelFaultCode::OperationMismatch,
902 "cancel_operation identity does not match the input envelope".to_string(),
903 None,
904 );
905 }
906 if cancelled_operation_id.is_empty()
907 || pending_call_ids.iter().any(|call_id| call_id.is_empty())
908 {
909 return self.fault_step(
910 operation_id,
911 event_id,
912 KernelFaultCode::InvalidConfig,
913 "cancel_operation requires non-empty operation and pending call identities"
914 .to_string(),
915 None,
916 );
917 }
918 if let Some(accepted) = &self.accepted_cancellation {
919 if accepted.reason == *reason && accepted.pending_call_ids == *pending_call_ids {
920 let step = accepted.step.clone();
921 self.recorded_events.insert(
922 event_id,
923 RecordedTransition {
924 fingerprint,
925 step: step.clone(),
926 },
927 );
928 self.record_accepted_input(
929 (mode == StepMode::Live).then_some(input),
930 input_bytes,
931 );
932 self.last_step = Some(step.clone());
933 return step;
934 }
935 return self.fault_step(
936 operation_id,
937 event_id,
938 KernelFaultCode::DuplicateEventConflict,
939 "cancel_operation conflicts with the committed cancellation".to_string(),
940 None,
941 );
942 }
943 }
944
945 let result_fingerprint = result_effect(&input.event).map(|(effect_id, _)| {
946 (
947 effect_id.to_string(),
948 serde_json::to_vec(&input.event)
949 .expect("KernelInputEvent serialization must succeed after typed construction"),
950 )
951 });
952 if let Some((effect_id, result_fingerprint)) = &result_fingerprint {
953 if let Some(completed) = self.completed_effects.get(effect_id) {
954 if completed.fingerprint == *result_fingerprint {
955 let step = completed.step.clone();
956 self.recorded_events.insert(
957 event_id,
958 RecordedTransition {
959 fingerprint,
960 step: step.clone(),
961 },
962 );
963 self.record_accepted_input(
964 (mode == StepMode::Live).then_some(input),
965 input_bytes,
966 );
967 self.last_step = Some(step.clone());
968 return step;
969 }
970 return self.fault_step(
971 operation_id,
972 event_id,
973 KernelFaultCode::UnexpectedEffectResult,
974 format!("effect result conflicts with the completed result: {effect_id}"),
975 Some(effect_id.clone()),
976 );
977 }
978 }
979
980 let lifecycle_transition = match self.lifecycle_transition(&input.event) {
981 Ok(transition) => transition,
982 Err(message) => {
983 return self.fault_step(
984 operation_id,
985 event_id,
986 KernelFaultCode::InvalidLifecycle,
987 message,
988 None,
989 );
990 }
991 };
992 if let KernelInputEvent::ConfigureRun { config } = &input.event {
993 if let Err(message) = validate_run_config(config, self.sm.ctx.max_tokens) {
994 return self.fault_step(
995 operation_id,
996 event_id,
997 KernelFaultCode::InvalidConfig,
998 message,
999 None,
1000 );
1001 }
1002 if config
1003 .reliability
1004 .as_ref()
1005 .and_then(|reliability| reliability.snapshot_input_limit)
1006 .is_some_and(|limit| limit <= self.accepted_input_count)
1007 {
1008 return self.fault_step(
1009 operation_id,
1010 event_id,
1011 KernelFaultCode::InvalidConfig,
1012 "snapshot_input_limit must leave room for the configure transaction"
1013 .to_string(),
1014 None,
1015 );
1016 }
1017 if config
1018 .reliability
1019 .as_ref()
1020 .and_then(|reliability| reliability.snapshot_journal_bytes_limit)
1021 .is_some_and(|limit| limit < self.accepted_input_bytes.saturating_add(input_bytes))
1022 {
1023 return self.fault_step(
1024 operation_id,
1025 event_id,
1026 KernelFaultCode::InvalidConfig,
1027 "snapshot_journal_bytes_limit must leave room for the configure transaction"
1028 .to_string(),
1029 None,
1030 );
1031 }
1032 if config
1033 .reliability
1034 .as_ref()
1035 .and_then(|reliability| reliability.max_input_bytes)
1036 .is_some_and(|limit| limit < input_bytes)
1037 {
1038 return self.fault_step(
1039 operation_id,
1040 event_id,
1041 KernelFaultCode::InvalidConfig,
1042 "max_input_bytes must admit the configure transaction".to_string(),
1043 None,
1044 );
1045 }
1046 }
1047 if let KernelInputEvent::DeliverSignal {
1048 delivery_id,
1049 attempt,
1050 ..
1051 } = &input.event
1052 {
1053 if delivery_id.is_empty() || *attempt == 0 {
1054 return self.fault_step(
1055 operation_id,
1056 event_id,
1057 KernelFaultCode::InvalidConfig,
1058 "deliver_signal requires a non-empty delivery_id and attempt >= 1".to_string(),
1059 None,
1060 );
1061 }
1062 }
1063 if let KernelInputEvent::SetSignalPolicy { policy } = &input.event {
1064 if let Err(message) = validate_signal_policy(policy) {
1065 return self.fault_step(
1066 operation_id,
1067 event_id,
1068 KernelFaultCode::InvalidConfig,
1069 message,
1070 None,
1071 );
1072 }
1073 }
1074 if let KernelInputEvent::QueryMemory { query } = &input.event {
1075 if let Err(message) = query.validate() {
1076 return self.fault_step(
1077 operation_id,
1078 event_id,
1079 KernelFaultCode::InvalidConfig,
1080 message,
1081 None,
1082 );
1083 }
1084 }
1085 if let KernelInputEvent::MemoryQueryResult {
1086 effect_id,
1087 hits,
1088 error,
1089 } = &input.event
1090 {
1091 if error.is_none() {
1092 let Some((query, requested_k)) = self.pending_memory_query.as_ref() else {
1093 return self.fault_step(
1094 operation_id,
1095 event_id,
1096 KernelFaultCode::UnexpectedEffectResult,
1097 "memory query result has no pending query".to_string(),
1098 Some(effect_id.clone()),
1099 );
1100 };
1101 if let Err(message) = query.validate_hits(hits, *requested_k) {
1102 return self.fault_step(
1103 operation_id,
1104 event_id,
1105 KernelFaultCode::UnexpectedEffectResult,
1106 message,
1107 Some(effect_id.clone()),
1108 );
1109 }
1110 }
1111 }
1112 if let KernelInputEvent::WorkflowSpawnResult {
1113 effect_id,
1114 started_agent_ids,
1115 failures,
1116 error,
1117 } = &input.event
1118 {
1119 if let Err(message) = self.sm.validate_workflow_spawn_result(
1120 started_agent_ids,
1121 failures,
1122 error.as_deref(),
1123 ) {
1124 return self.fault_step(
1125 operation_id,
1126 event_id,
1127 KernelFaultCode::UnexpectedEffectResult,
1128 message,
1129 Some(effect_id.clone()),
1130 );
1131 }
1132 }
1133 if let KernelInputEvent::LargeResultSpoolResult {
1134 effect_id,
1135 spool_ref,
1136 error,
1137 } = &input.event
1138 {
1139 if error.is_none() && spool_ref.as_deref().map_or(true, str::is_empty) {
1140 return self.fault_step(
1141 operation_id,
1142 event_id,
1143 KernelFaultCode::UnexpectedEffectResult,
1144 "successful spool result requires a non-empty spool_ref".to_string(),
1145 Some(effect_id.clone()),
1146 );
1147 }
1148 }
1149
1150 if let Some((effect_id, expected_kind)) = result_effect(&input.event) {
1151 match self.pending_effects.get(effect_id) {
1152 Some(actual_kind) if actual_kind == &expected_kind => {
1153 self.pending_effects.remove(effect_id);
1154 }
1155 _ => {
1156 return self.fault_step(
1157 operation_id,
1158 event_id,
1159 KernelFaultCode::UnexpectedEffectResult,
1160 format!("effect result does not match a pending effect: {effect_id}"),
1161 Some(effect_id.to_string()),
1162 );
1163 }
1164 }
1165 }
1166
1167 if self.operation_id.is_none() {
1168 self.operation_id = Some(operation_id.clone());
1169 }
1170
1171 if input.observed_at_ms > 0 {
1172 self.sm.set_observed_time(input.observed_at_ms);
1173 }
1174
1175 let accepted_input = (mode == StepMode::Live).then(|| input.clone());
1176 let step_seq = self.allocate_step_seq();
1177 let cancellation_identity = match &input.event {
1178 KernelInputEvent::CancelOperation {
1179 reason,
1180 pending_call_ids,
1181 ..
1182 } => Some((*reason, pending_call_ids.clone())),
1183 _ => None,
1184 };
1185 let mut step = self.dispatch(
1186 StepIdentity {
1187 operation_id: operation_id.clone(),
1188 input_event_id: event_id.clone(),
1189 step_seq,
1190 },
1191 input.event,
1192 );
1193 let reservation_id = self
1194 .sm
1195 .budget_grant()
1196 .map(|grant| grant.reservation_id.clone());
1197 for observation in &mut step.observations {
1198 if let KernelObservation::BudgetExceeded {
1199 operation_id: observed_operation_id,
1200 reservation_id: observed_reservation_id,
1201 ..
1202 } = observation
1203 {
1204 *observed_operation_id = operation_id.clone();
1205 if observed_reservation_id.is_none() {
1206 *observed_reservation_id = reservation_id.clone();
1207 }
1208 }
1209 }
1210 let is_terminal = step
1211 .actions
1212 .iter()
1213 .any(|action| matches!(action.effect, KernelEffect::Done { .. }));
1214 if is_terminal && !self.budget_usage_reported {
1215 if let Some(reservation_id) = reservation_id {
1216 let (tokens, subagents, rounds) = self.sm.local_budget_usage();
1217 step.observations
1218 .push(KernelObservation::BudgetUsageReported {
1219 operation_id: operation_id.clone(),
1220 reservation_id,
1221 tokens,
1222 subagents,
1223 rounds,
1224 });
1225 self.budget_usage_reported = true;
1226 }
1227 }
1228 self.advance_lifecycle(lifecycle_transition, &step);
1229 if is_terminal {
1230 self.pending_effects.clear();
1231 } else {
1232 for action in &step.actions {
1233 if let Some(kind) = pending_effect_kind(&action.effect) {
1234 self.pending_effects
1235 .retain(|_, pending_kind| *pending_kind != kind);
1236 self.pending_effects.insert(action.effect_id.clone(), kind);
1237 }
1238 }
1239 }
1240 if let Some((effect_id, result_fingerprint)) = result_fingerprint {
1241 self.completed_effects.insert(
1242 effect_id,
1243 RecordedTransition {
1244 fingerprint: result_fingerprint,
1245 step: step.clone(),
1246 },
1247 );
1248 }
1249 if let Some((reason, pending_call_ids)) = cancellation_identity {
1250 self.accepted_cancellation = Some(AcceptedCancellation {
1251 reason,
1252 pending_call_ids,
1253 step: step.clone(),
1254 });
1255 }
1256 self.recorded_events.insert(
1257 event_id,
1258 RecordedTransition {
1259 fingerprint,
1260 step: step.clone(),
1261 },
1262 );
1263 self.record_accepted_input(accepted_input, input_bytes);
1264 self.last_step = Some(step.clone());
1265 step
1266 }
1267
1268 fn ensure_snapshot_available(&self) -> Result<(), KernelFault> {
1269 if self.poisoned {
1270 return Err(transaction_conflict_fault(
1271 None,
1272 "kernel runtime was invalidated by a transaction consistency error".to_string(),
1273 ));
1274 }
1275 if let Some(candidate) = &self.prepared {
1276 return Err(transaction_conflict_fault(
1277 candidate.state.operation_id.clone(),
1278 "cannot snapshot an uncommitted prepared transition".to_string(),
1279 ));
1280 }
1281 if self.snapshot_overflowed
1282 || self.accepted_input_count > self.snapshot_input_limit
1283 || self.accepted_input_bytes > self.snapshot_journal_bytes_limit
1284 {
1285 return Err(snapshot_fault(
1286 self.operation_id.clone(),
1287 format!(
1288 "snapshot input journal exceeded configured limits: {} inputs / {} bytes",
1289 self.snapshot_input_limit, self.snapshot_journal_bytes_limit
1290 ),
1291 ));
1292 }
1293 Ok(())
1294 }
1295
1296 fn record_accepted_input(&mut self, input: Option<KernelInput>, input_bytes: usize) {
1297 if self.accepted_input_count < self.snapshot_input_limit
1298 && self.accepted_input_bytes.saturating_add(input_bytes)
1299 <= self.snapshot_journal_bytes_limit
1300 {
1301 self.accepted_input_count += 1;
1302 self.accepted_input_bytes += input_bytes;
1303 if let Some(input) = input {
1304 self.accepted_inputs.push(input);
1305 }
1306 } else {
1307 self.snapshot_overflowed = true;
1308 }
1309 }
1310
1311 fn dispatch(&mut self, identity: StepIdentity, event: KernelInputEvent) -> KernelStep {
1312 let action = match event {
1313 KernelInputEvent::SetTools { tools } => {
1314 self.sm.tools = tools;
1315 return identity.empty(self.sm.take_observations());
1316 }
1317 KernelInputEvent::SetAvailableSkills { skills } => {
1318 self.sm.ctx.set_available_skills(skills);
1319 return identity.empty(self.sm.take_observations());
1320 }
1321 KernelInputEvent::SkillActivated { name, lease_turns } => {
1322 let expires_at_turn = lease_turns.map(|n| self.sm.turn.saturating_add(n));
1326 self.sm.ctx.activate_skill_leased(name, expires_at_turn);
1327 return identity.empty(self.sm.take_observations());
1328 }
1329 KernelInputEvent::SkillDeactivated { name } => {
1330 self.sm.ctx.deactivate_skill(&name);
1331 return identity.empty(self.sm.take_observations());
1332 }
1333 KernelInputEvent::SetStableCoreTools { tool_ids } => {
1334 self.sm
1335 .ctx
1336 .set_stable_core_tools(tool_ids.into_iter().map(Into::into));
1337 return identity.empty(self.sm.take_observations());
1338 }
1339 KernelInputEvent::SetMemoryEnabled { enabled } => {
1340 self.sm.ctx.set_memory_enabled(enabled);
1341 return identity.empty(self.sm.take_observations());
1342 }
1343 KernelInputEvent::SetKnowledgeEnabled { enabled } => {
1344 self.sm.ctx.set_knowledge_enabled(enabled);
1345 return identity.empty(self.sm.take_observations());
1346 }
1347 KernelInputEvent::SetPlanToolEnabled { enabled } => {
1348 self.sm.ctx.set_plan_tool_enabled(enabled);
1349 return identity.empty(self.sm.take_observations());
1350 }
1351 KernelInputEvent::SetTokenizer { .. } => {
1352 self.sm.ctx.engine = ContextTokenEngine::char_approx();
1356 return identity.empty(self.sm.take_observations());
1357 }
1358 KernelInputEvent::AddSystemMessage { content, tokens } => {
1359 self.sm
1360 .ctx
1361 .partitions
1362 .system
1363 .push(Message::system(content), tokens.max(1));
1364 return identity.empty(self.sm.take_observations());
1365 }
1366 KernelInputEvent::AddKnowledgeMessage {
1367 content,
1368 tokens,
1369 key,
1370 pinned,
1371 } => {
1372 self.sm.ctx.push_knowledge_entry(
1385 key.map(compact_str::CompactString::from),
1386 Message::system(content),
1387 tokens.max(1),
1388 pinned,
1389 );
1390 return identity.empty(self.sm.take_observations());
1391 }
1392 KernelInputEvent::RemoveKnowledge { key } => {
1393 self.sm.ctx.remove_knowledge(&key);
1394 return identity.empty(self.sm.take_observations());
1395 }
1396 KernelInputEvent::AddHistoryMessage { message, tokens } => {
1397 let tokens = tokens.unwrap_or_else(|| self.sm.ctx.engine.count_message(&message));
1398 self.sm.ctx.push_history(message, tokens.max(1));
1399 return identity.empty(self.sm.take_observations());
1400 }
1401 KernelInputEvent::PreloadHistory { messages } => {
1402 self.sm.preload_history(messages);
1403 return identity.empty(self.sm.take_observations());
1404 }
1405 KernelInputEvent::MountCapability { capability } => {
1406 self.sm.mount_capability(capability, None, None);
1407 return identity.empty(self.sm.take_observations());
1408 }
1409 KernelInputEvent::UnmountCapability {
1410 capability_kind,
1411 id,
1412 } => {
1413 self.sm.unmount_capability(capability_kind, &id);
1414 return identity.empty(self.sm.take_observations());
1415 }
1416 KernelInputEvent::LoadMilestoneContract { contract } => {
1417 self.sm.load_milestone_contract(contract);
1418 return identity.empty(self.sm.take_observations());
1419 }
1420 KernelInputEvent::LoadGovernancePolicy {
1421 default_action,
1422 rules,
1423 vetoed_tools,
1424 rate_limits,
1425 constraints,
1426 } => {
1427 self.sm.set_governance(build_governance_pipeline(
1428 default_action,
1429 rules,
1430 vetoed_tools,
1431 rate_limits,
1432 constraints,
1433 ));
1434 return identity.empty(self.sm.take_observations());
1435 }
1436 KernelInputEvent::ConfigureRun { config } => {
1437 let RunConfig {
1444 tools,
1445 available_skills,
1446 stable_core_tools,
1447 memory_enabled,
1448 knowledge_enabled,
1449 plan_tool_enabled,
1450 tokenizer,
1451 governance,
1452 signal_policy,
1453 prompt_budget,
1454 context_policy,
1455 scheduler_policy,
1456 resource_quota,
1457 budget_grant,
1458 repeat_fuse,
1459 criteria_gate,
1460 knowledge_budget_ratio,
1461 entropy_watch,
1462 reliability,
1463 } = config;
1464 if let Some(tools) = tools {
1465 self.sm.tools = tools;
1466 }
1467 if let Some(skills) = available_skills {
1468 self.sm.ctx.set_available_skills(skills);
1469 }
1470 if let Some(ids) = stable_core_tools {
1471 self.sm
1472 .ctx
1473 .set_stable_core_tools(ids.into_iter().map(Into::into));
1474 }
1475 if let Some(enabled) = memory_enabled {
1476 self.sm.ctx.set_memory_enabled(enabled);
1477 }
1478 if let Some(enabled) = knowledge_enabled {
1479 self.sm.ctx.set_knowledge_enabled(enabled);
1480 }
1481 if let Some(enabled) = plan_tool_enabled {
1482 self.sm.ctx.set_plan_tool_enabled(enabled);
1483 }
1484 if tokenizer.is_some() {
1485 self.sm.ctx.engine = ContextTokenEngine::char_approx();
1486 }
1487 if let Some(g) = governance {
1488 self.sm.set_governance(build_governance_pipeline(
1489 g.default_action,
1490 g.rules,
1491 g.vetoed_tools,
1492 g.rate_limits,
1493 g.constraints,
1494 ));
1495 }
1496 if let Some(policy) = signal_policy {
1497 self.sm.set_signal_policy(
1498 policy.queue_max as usize,
1499 policy.ttl_ms,
1500 policy.deadline_escalation.unwrap_or(false),
1501 );
1502 }
1503 if let Some(prompt_budget) = prompt_budget {
1504 self.sm.ctx.set_prompt_budget(prompt_budget);
1505 }
1506 if let Some(context_policy) = context_policy {
1507 self.sm.ctx.apply_context_policy(&context_policy);
1508 }
1509 if let Some(policy) = scheduler_policy {
1510 self.sm.set_scheduler_policy(policy);
1511 }
1512 if let Some(quota) = resource_quota {
1513 self.sm.set_resource_quota(quota);
1514 }
1515 if let Some(grant) = budget_grant {
1516 self.sm.set_budget_grant(grant);
1517 }
1518 if let Some(fuse) = repeat_fuse {
1519 self.sm.set_repeat_fuse(fuse);
1520 }
1521 if let Some(enabled) = criteria_gate {
1522 self.sm.set_criteria_gate(enabled);
1523 }
1524 if let Some(ratio) = knowledge_budget_ratio {
1525 self.sm.ctx.config.knowledge_budget_ratio = ratio;
1526 }
1527 if let Some(watch) = entropy_watch {
1528 self.sm.set_entropy_watch(watch);
1529 }
1530 if let Some(reliability) = reliability {
1531 if let Some(capacity) = reliability.event_replay_capacity {
1532 self.recorded_events.set_capacity(capacity);
1533 }
1534 if let Some(capacity) = reliability.completed_effect_replay_capacity {
1535 self.completed_effects.set_capacity(capacity);
1536 }
1537 if let Some(limit) = reliability.snapshot_input_limit {
1538 self.snapshot_input_limit = limit;
1539 }
1540 if let Some(limit) = reliability.max_input_bytes {
1541 self.max_input_bytes = limit;
1542 }
1543 if let Some(limit) = reliability.snapshot_journal_bytes_limit {
1544 self.snapshot_journal_bytes_limit = limit;
1545 }
1546 self.sm.set_reliability_config(&reliability);
1547 }
1548 return identity.empty(self.sm.take_observations());
1549 }
1550 KernelInputEvent::SetSignalPolicy { policy } => {
1551 self.sm.set_signal_policy(
1552 policy.queue_max as usize,
1553 policy.ttl_ms,
1554 policy.deadline_escalation.unwrap_or(false),
1555 );
1556 return identity.empty(self.sm.take_observations());
1557 }
1558 KernelInputEvent::PageIn { entries } => {
1559 self.sm.apply_page_in(&entries);
1560 return identity.empty(self.sm.take_observations());
1561 }
1562 KernelInputEvent::ForceCompact => {
1563 self.sm.force_compact();
1564 self.sm
1565 .externalize_pending_host_effect(LoopAction::AwaitingResume)
1566 }
1567 KernelInputEvent::UpdateTask { update } => {
1568 self.sm.ctx.update_task(update);
1569 return identity.empty(self.sm.take_observations());
1570 }
1571 KernelInputEvent::StartRun { task, run_spec } => {
1572 self.sm.run_spec = run_spec;
1573 self.sm.start(task)
1574 }
1575 KernelInputEvent::CapabilityCommand { command } => {
1576 self.sm.execute_capability_command(command);
1577 return identity.empty(self.sm.take_observations());
1578 }
1579 KernelInputEvent::Resume => self.sm.resume_after_preload(),
1580 KernelInputEvent::ApprovalResult {
1581 effect_id: _,
1582 approved_calls,
1583 denied_calls,
1584 error,
1585 } => match error {
1586 Some(error) => self.sm.retry_approval(error),
1587 None => self.sm.resolve_approval(approved_calls, denied_calls),
1588 },
1589 KernelInputEvent::WorkflowSpawnResult {
1590 effect_id: _,
1591 started_agent_ids,
1592 failures,
1593 error,
1594 } => match error {
1595 Some(error) => self.sm.retry_workflow_spawn(error),
1596 None => self.sm.resolve_workflow_spawn(started_agent_ids, failures),
1597 },
1598 KernelInputEvent::PreemptResult {
1599 effect_id: _,
1600 error,
1601 } => match error {
1602 Some(error) => self.sm.retry_preempt(error),
1603 None => self.sm.resolve_preempt(),
1604 },
1605 KernelInputEvent::MemoryPersistResult {
1606 effect_id: _,
1607 error,
1608 } => {
1609 let memory = self
1610 .pending_memory_write
1611 .take()
1612 .expect("validated memory result requires pending write");
1613 let staged_store = self
1614 .pending_memory_store
1615 .take()
1616 .expect("validated memory result requires staged scoped upsert");
1617 let turn = self.sm.turn;
1618 match error {
1619 Some(error) => {
1620 self.sm
1621 .observations
1622 .push(KernelObservation::MemoryWriteFailed {
1623 turn,
1624 record_id: memory.record_id,
1625 error,
1626 })
1627 }
1628 None => {
1629 self.memory_records = staged_store;
1630 self.sm.observations.push(KernelObservation::MemoryWritten {
1631 turn,
1632 record_id: memory.record_id,
1633 scope: memory.scope,
1634 memory_kind: memory.kind,
1635 name: memory.name,
1636 size_bytes: memory.content.len() as u32,
1637 });
1638 }
1639 }
1640 return identity.empty(self.sm.take_observations());
1641 }
1642 KernelInputEvent::MemoryQueryResult {
1643 effect_id: _,
1644 hits,
1645 error,
1646 } => {
1647 let (query, requested_k) = self
1648 .pending_memory_query
1649 .take()
1650 .expect("validated memory result requires pending query");
1651 let turn = self.sm.turn;
1652 let continuation = match error {
1653 Some(error) => {
1654 let continuation = self.sm.resume_after_preload();
1655 self.sm
1656 .observations
1657 .push(KernelObservation::MemoryQueryFailed {
1658 turn,
1659 scope: query.scope,
1660 query: query.query,
1661 error,
1662 });
1663 continuation
1664 }
1665 None => {
1666 let promotion_threshold = self
1673 .sm
1674 .memory_policy()
1675 .and_then(|policy| policy.promotion_recall_threshold);
1676 let mut recalls = Vec::new();
1677 let mut promotions = Vec::new();
1678 for hit in &hits {
1679 let trust = match hit.record.provenance.trust {
1680 crate::mm::memory::MemoryTrustLevel::Untrusted => "untrusted",
1681 crate::mm::memory::MemoryTrustLevel::UserAsserted => {
1682 "user_asserted"
1683 }
1684 crate::mm::memory::MemoryTrustLevel::HostVerified => {
1685 "host_verified"
1686 }
1687 };
1688 let content = format!(
1689 "[MEMORY record_id={} trust={} score={:.3}] {}",
1690 hit.record.record_id, trust, hit.score, hit.record.content
1691 );
1692 let tokens = self.sm.ctx.engine.count(&content).max(1);
1693 self.sm.ctx.push_history(
1694 crate::types::message::Message::user(content),
1695 tokens,
1696 );
1697
1698 let record_id = hit.record.record_id.clone();
1699 let before = hit.record.recall_count;
1700 let after = before.saturating_add(1);
1701 recalls.push(crate::mm::memory::MemoryRecallLifecycle {
1702 record_id: record_id.clone(),
1703 recall_count: after,
1704 last_recalled_at: turn as u64,
1705 });
1706 if let Some(threshold) = promotion_threshold {
1707 if before < threshold && after >= threshold {
1708 promotions.push((record_id, after));
1709 }
1710 }
1711 }
1712 let continuation = self.sm.resume_after_preload();
1713 self.sm.observations.push(KernelObservation::MemoryQueried {
1714 turn,
1715 scope: query.scope.clone(),
1716 query: query.query,
1717 requested_k,
1718 requires_async_response: false,
1719 });
1720 if !recalls.is_empty() {
1721 self.sm.observations.push(KernelObservation::MemoryRecalled {
1722 turn,
1723 scope: query.scope,
1724 recalls,
1725 });
1726 }
1727 for (record_id, recall_count) in promotions {
1728 self.sm
1729 .observations
1730 .push(KernelObservation::PromotionSuggested {
1731 turn,
1732 record_id,
1733 recall_count,
1734 });
1735 }
1736 continuation
1737 }
1738 };
1739 return identity.single(continuation, self.sm.take_observations());
1743 }
1744 KernelInputEvent::LargeResultSpoolResult {
1745 effect_id: _,
1746 spool_ref,
1747 error,
1748 } => self.sm.resolve_large_result_spool(spool_ref, error),
1749 KernelInputEvent::PageOutArchiveResult {
1750 effect_id: _,
1751 archive_ref,
1752 error,
1753 } => self.sm.resolve_page_out_archive(archive_ref, error),
1754 KernelInputEvent::SetSchedulerBudget { max_wall_ms } => {
1755 self.sm.set_wall_budget(max_wall_ms);
1756 return identity.empty(self.sm.take_observations());
1757 }
1758 KernelInputEvent::SetResourceQuota { quota } => {
1759 self.sm.set_resource_quota(quota);
1760 return identity.empty(self.sm.take_observations());
1761 }
1762 KernelInputEvent::SetCriteriaGate { enabled } => {
1763 self.sm.set_criteria_gate(enabled);
1764 return identity.empty(self.sm.take_observations());
1765 }
1766 KernelInputEvent::SetKnowledgeBudget { ratio } => {
1767 self.sm.ctx.config.knowledge_budget_ratio = ratio;
1768 return identity.empty(self.sm.take_observations());
1769 }
1770 KernelInputEvent::SetRepeatFuse {
1771 enabled,
1772 deny_after,
1773 terminate_after,
1774 } => {
1775 let mut cfg = self.sm.repeat_fuse_config();
1776 if let Some(e) = enabled {
1777 cfg.enabled = e;
1778 }
1779 if let Some(d) = deny_after {
1780 cfg.deny_after = d;
1781 }
1782 if let Some(t) = terminate_after {
1783 cfg.terminate_after = t;
1784 }
1785 self.sm.set_repeat_fuse(cfg);
1786 return identity.empty(self.sm.take_observations());
1787 }
1788 KernelInputEvent::SetEntropyWatch {
1789 enabled,
1790 threshold,
1791 hysteresis,
1792 cooldown_turns,
1793 notify_model,
1794 } => {
1795 let mut cfg = self.sm.entropy_watch_config();
1796 if let Some(e) = enabled {
1797 cfg.enabled = e;
1798 }
1799 if let Some(t) = threshold {
1800 cfg.threshold = t;
1801 }
1802 if let Some(h) = hysteresis {
1803 cfg.hysteresis = h;
1804 }
1805 if let Some(c) = cooldown_turns {
1806 cfg.cooldown_turns = c;
1807 }
1808 if let Some(n) = notify_model {
1809 cfg.notify_model = n;
1810 }
1811 self.sm.set_entropy_watch(cfg);
1812 return identity.empty(self.sm.take_observations());
1813 }
1814 KernelInputEvent::ProviderResult {
1815 effect_id: _,
1816 message,
1817 observed_input_tokens,
1818 observed_output_tokens: _,
1819 now_ms,
1820 stop_reason,
1821 } => {
1822 if let Some(tokens) = observed_input_tokens {
1823 self.sm.ctx.set_observed_prompt_tokens(tokens);
1824 }
1825 if let Some(ms) = now_ms {
1828 self.sm.set_observed_time(ms);
1829 }
1830 self.sm.set_pending_stop_reason(stop_reason);
1832 self.sm.feed(LoopEvent::LLMResponse { message })
1833 }
1834 KernelInputEvent::ToolResults {
1835 effect_id: _,
1836 results,
1837 } => self.sm.feed(LoopEvent::ToolResults { results }),
1838 KernelInputEvent::ProviderError {
1839 effect_id: _,
1840 message,
1841 } => {
1842 self.sm.recover_from_provider_error(&message)
1845 }
1846 KernelInputEvent::DeliverSignal {
1847 delivery_id,
1848 attempt,
1849 signal,
1850 } => match self.sm.signal_event(
1851 identity.operation_id.clone(),
1852 delivery_id,
1853 attempt,
1854 signal,
1855 ) {
1856 Some(action) => action,
1857 None => {
1860 return identity.empty(self.sm.take_observations());
1861 }
1862 },
1863 KernelInputEvent::MilestoneResult {
1864 effect_id: _,
1865 result,
1866 } => self.sm.feed(LoopEvent::MilestoneResult { result }),
1867 KernelInputEvent::SpawnSubAgent {
1868 spec,
1869 parent_session_id,
1870 } => self.sm.spawn_sub_agent(spec, &parent_session_id),
1871 KernelInputEvent::LoadWorkflow {
1872 spec,
1873 parent_session_id,
1874 resumed_submissions,
1875 resumed_submission_bases,
1876 resumed_outcomes,
1877 } => {
1878 if resumed_outcomes.is_empty() && resumed_submissions.is_empty() {
1879 self.sm.load_workflow(spec, &parent_session_id)
1880 } else {
1881 self.sm.load_workflow_resumed(
1882 spec,
1883 &parent_session_id,
1884 &resumed_submissions,
1885 &resumed_submission_bases,
1886 &resumed_outcomes,
1887 )
1888 }
1889 }
1890 KernelInputEvent::SubAgentCompleted { result } => {
1891 self.sm.feed(LoopEvent::SubAgentCompleted { result })
1892 }
1893 KernelInputEvent::SubmitWorkflowNodes {
1894 nodes,
1895 submitter_agent_id,
1896 } => self
1897 .sm
1898 .submit_workflow_nodes(nodes, submitter_agent_id.as_deref()),
1899 KernelInputEvent::SubmitWorkflow {
1900 spec,
1901 parent_session_id,
1902 submitter_agent_id,
1903 } => self
1904 .sm
1905 .submit_workflow(spec, &parent_session_id, submitter_agent_id.as_deref()),
1906 KernelInputEvent::SetMemoryPolicy {
1907 memory_path,
1908 stale_warning_days,
1909 retrieval_top_k,
1910 validation_enabled,
1911 max_content_bytes,
1912 max_name_length,
1913 promotion_recall_threshold,
1914 } => {
1915 self.sm.set_memory_policy(crate::mm::memory::MemoryPolicy {
1921 memory_path,
1922 stale_warning_days,
1923 retrieval_top_k,
1924 validation_enabled,
1925 max_content_bytes,
1926 max_name_length,
1927 promotion_recall_threshold,
1928 });
1929 return identity.empty(self.sm.take_observations());
1930 }
1931 KernelInputEvent::WriteMemory { memory } => {
1932 use crate::mm::memory::validate_memory_write;
1935 let turn = self.sm.turn;
1936 let disposition = self
1940 .sm
1941 .gate_syscall(&crate::syscall::Syscall::WriteMemory(memory.clone()));
1942 if !disposition.is_allowed() {
1943 let error = match disposition {
1944 crate::syscall::Disposition::RateLimited { retry_after_ms } => {
1945 format!("memory write rate limited; retry after {retry_after_ms}ms")
1946 }
1947 crate::syscall::Disposition::Deny { reason, .. } => {
1948 format!("memory write denied: {reason}")
1949 }
1950 _ => "memory write not permitted".to_string(),
1951 };
1952 self.sm
1953 .observations
1954 .push(KernelObservation::MemoryValidationFailed {
1955 turn,
1956 record_id: memory.record_id.clone(),
1957 error,
1958 });
1959 return identity.empty(self.sm.take_observations());
1960 }
1961 let validation_result = match self.sm.memory_policy() {
1965 Some(p) if !p.validation_enabled => Ok(()),
1966 Some(p) => p.validation().validate(&memory),
1967 None => validate_memory_write(&memory),
1968 };
1969 match validation_result {
1970 Ok(()) => {
1971 let key = memory.key();
1972 let mut staged_store = self.memory_records.clone();
1973 if let Err(error) = staged_store.upsert(memory) {
1974 self.sm
1975 .observations
1976 .push(KernelObservation::MemoryValidationFailed {
1977 turn,
1978 record_id: error.record_id().to_string(),
1979 error: format!("record id conflicts with another scoped key"),
1980 });
1981 return identity.empty(self.sm.take_observations());
1982 }
1983 let canonical = staged_store
1984 .get(&key.scope, key.kind, &key.name)
1985 .expect("scoped upsert must retain its canonical record")
1986 .clone();
1987 self.pending_memory_write = Some(canonical.clone());
1988 self.pending_memory_store = Some(staged_store);
1989 LoopAction::PersistMemory { memory: canonical }
1990 }
1991 Err(err) => {
1992 use crate::mm::memory::MemoryValidationError;
1994 let error_msg = match err {
1995 MemoryValidationError::MissingRequiredField { field } => {
1996 format!("Missing required field: {}", field)
1997 }
1998 MemoryValidationError::ContentTooLarge { size, limit } => {
1999 format!("Content too large: {} bytes (limit: {})", size, limit)
2000 }
2001 MemoryValidationError::ForbiddenPattern { pattern, reason } => {
2002 format!("Forbidden pattern '{}': {}", pattern, reason)
2003 }
2004 MemoryValidationError::InvalidKind { kind } => {
2005 format!("Invalid kind: {}", kind)
2006 }
2007 MemoryValidationError::NameTooLong { length, limit } => {
2008 format!("Name too long: {} chars (limit: {})", length, limit)
2009 }
2010 };
2011 self.sm
2012 .observations
2013 .push(KernelObservation::MemoryValidationFailed {
2014 turn,
2015 record_id: memory.record_id.clone(),
2016 error: error_msg,
2017 });
2018 return identity.empty(self.sm.take_observations());
2019 }
2020 }
2021 }
2022 KernelInputEvent::QueryMemory { query } => {
2023 let requested_k = match self.sm.memory_policy() {
2027 Some(p) => p.clamp_top_k(query.top_k),
2028 None => query.top_k,
2029 };
2030 self.pending_memory_query = Some((query.clone(), requested_k));
2031 LoopAction::QueryMemory { query, requested_k }
2032 }
2033 KernelInputEvent::CompleteRun => self.sm.feed(LoopEvent::Complete),
2034 KernelInputEvent::CancelOperation {
2035 operation_id,
2036 reason,
2037 pending_call_ids,
2038 } => {
2039 self.pending_memory_write = None;
2040 self.pending_memory_store = None;
2041 self.pending_memory_query = None;
2042 self.sm
2043 .cancel_operation(operation_id, reason, pending_call_ids)
2044 }
2045 };
2046 let action = self.sm.externalize_pending_host_effect(action);
2047 if matches!(action, LoopAction::AwaitingResume) {
2048 return identity.empty(self.sm.take_observations());
2049 }
2050 identity.single(action, self.sm.take_observations())
2051 }
2052
2053 fn lifecycle_transition(
2054 &self,
2055 event: &KernelInputEvent,
2056 ) -> Result<LifecycleTransition, String> {
2057 if self.lifecycle.is_terminal() && !matches!(event, KernelInputEvent::DeliverSignal { .. })
2058 {
2059 return Err(format!(
2060 "kernel is terminal in lifecycle {:?}",
2061 self.lifecycle
2062 ));
2063 }
2064
2065 match event {
2066 KernelInputEvent::ConfigureRun { .. } => match self.lifecycle {
2067 KernelLifecycle::Created | KernelLifecycle::Configured => {
2068 Ok(LifecycleTransition::Configure)
2069 }
2070 _ => Err(format!(
2071 "configure_run is not valid in lifecycle {:?}",
2072 self.lifecycle
2073 )),
2074 },
2075 KernelInputEvent::SetSignalPolicy { .. } => match self.lifecycle {
2076 KernelLifecycle::Created | KernelLifecycle::Configured => {
2077 Ok(LifecycleTransition::Configure)
2078 }
2079 _ => Err(format!(
2080 "set_signal_policy is not valid in lifecycle {:?}",
2081 self.lifecycle
2082 )),
2083 },
2084 KernelInputEvent::StartRun { .. } => match self.lifecycle {
2085 KernelLifecycle::Created | KernelLifecycle::Configured => {
2086 Ok(LifecycleTransition::Start)
2087 }
2088 _ => Err(format!(
2089 "start_run is not valid in lifecycle {:?}",
2090 self.lifecycle
2091 )),
2092 },
2093 KernelInputEvent::Resume => match self.lifecycle {
2094 KernelLifecycle::Configured => Ok(LifecycleTransition::Resume),
2095 _ => Err(format!(
2096 "resume is not valid in lifecycle {:?}",
2097 self.lifecycle
2098 )),
2099 },
2100 KernelInputEvent::ApprovalResult { .. }
2101 | KernelInputEvent::WorkflowSpawnResult { .. }
2102 | KernelInputEvent::PreemptResult { .. } => match self.lifecycle {
2103 KernelLifecycle::Suspended => Ok(LifecycleTransition::Resume),
2104 _ => Err(format!(
2105 "effect result is not valid in lifecycle {:?}",
2106 self.lifecycle
2107 )),
2108 },
2109 KernelInputEvent::MemoryPersistResult { .. }
2110 | KernelInputEvent::LargeResultSpoolResult { .. }
2111 | KernelInputEvent::PageOutArchiveResult { .. } => match self.lifecycle {
2112 KernelLifecycle::Configured | KernelLifecycle::Running => {
2113 Ok(LifecycleTransition::Stay)
2114 }
2115 _ => Err(format!(
2116 "memory effect result is not valid in lifecycle {:?}",
2117 self.lifecycle
2118 )),
2119 },
2120 KernelInputEvent::MemoryQueryResult { .. } => match self.lifecycle {
2121 KernelLifecycle::Configured | KernelLifecycle::Running => {
2122 Ok(LifecycleTransition::Resume)
2123 }
2124 _ => Err(format!(
2125 "memory query result is not valid in lifecycle {:?}",
2126 self.lifecycle
2127 )),
2128 },
2129 KernelInputEvent::ProviderResult { .. }
2130 | KernelInputEvent::ProviderError { .. }
2131 | KernelInputEvent::ToolResults { .. }
2132 | KernelInputEvent::MilestoneResult { .. }
2133 | KernelInputEvent::LoadWorkflow { .. }
2134 | KernelInputEvent::SpawnSubAgent { .. } => match self.lifecycle {
2135 KernelLifecycle::Running => Ok(LifecycleTransition::Stay),
2136 _ => Err(format!(
2137 "execution input is not valid in lifecycle {:?}",
2138 self.lifecycle
2139 )),
2140 },
2141 KernelInputEvent::SubmitWorkflow { .. }
2142 | KernelInputEvent::SubmitWorkflowNodes { .. }
2143 | KernelInputEvent::CompleteRun
2144 | KernelInputEvent::CancelOperation { .. } => match self.lifecycle {
2145 KernelLifecycle::Running | KernelLifecycle::Suspended => {
2146 Ok(LifecycleTransition::Stay)
2147 }
2148 _ => Err(format!(
2149 "execution input is not valid in lifecycle {:?}",
2150 self.lifecycle
2151 )),
2152 },
2153 KernelInputEvent::DeliverSignal { .. } => match self.lifecycle {
2154 KernelLifecycle::Running
2155 | KernelLifecycle::Suspended
2156 | KernelLifecycle::Completed
2157 | KernelLifecycle::Failed
2158 | KernelLifecycle::Cancelled => Ok(LifecycleTransition::Stay),
2159 _ => Err(format!(
2160 "deliver_signal is not valid in lifecycle {:?}",
2161 self.lifecycle
2162 )),
2163 },
2164 KernelInputEvent::SubAgentCompleted { .. } => match self.lifecycle {
2165 KernelLifecycle::Running | KernelLifecycle::Suspended => {
2166 Ok(LifecycleTransition::Resume)
2167 }
2168 _ => Err(format!(
2169 "sub_agent_completed is not valid in lifecycle {:?}",
2170 self.lifecycle
2171 )),
2172 },
2173 _ => match self.lifecycle {
2174 KernelLifecycle::Suspended => Err(
2175 "only resume, sub_agent_completed, or cancellation is valid while suspended"
2176 .to_string(),
2177 ),
2178 KernelLifecycle::Created => Ok(LifecycleTransition::Configure),
2179 _ => Ok(LifecycleTransition::Stay),
2180 },
2181 }
2182 }
2183
2184 fn advance_lifecycle(&mut self, transition: LifecycleTransition, step: &KernelStep) {
2185 if let Some(result) = step.actions.iter().find_map(|action| match &action.effect {
2186 KernelEffect::Done { result } => Some(result),
2187 _ => None,
2188 }) {
2189 self.lifecycle = match result.termination {
2190 crate::types::result::TerminationReason::Completed => KernelLifecycle::Completed,
2191 crate::types::result::TerminationReason::UserAbort => KernelLifecycle::Cancelled,
2192 _ => KernelLifecycle::Failed,
2193 };
2194 return;
2195 }
2196
2197 if self.sm.is_suspended() {
2198 self.lifecycle = KernelLifecycle::Suspended;
2199 return;
2200 }
2201
2202 self.lifecycle = match transition {
2203 LifecycleTransition::Configure if self.lifecycle == KernelLifecycle::Created => {
2204 KernelLifecycle::Configured
2205 }
2206 LifecycleTransition::Start | LifecycleTransition::Resume => KernelLifecycle::Running,
2207 _ => self.lifecycle,
2208 };
2209 }
2210
2211 fn allocate_step_seq(&mut self) -> u64 {
2212 let step_seq = self.next_step_seq;
2213 self.next_step_seq = self.next_step_seq.saturating_add(1);
2214 step_seq
2215 }
2216
2217 fn fault_step(
2218 &mut self,
2219 operation_id: String,
2220 event_id: String,
2221 code: KernelFaultCode,
2222 message: String,
2223 effect_id: Option<String>,
2224 ) -> KernelStep {
2225 let step_seq = self.next_step_seq;
2228 KernelStep::fault(
2229 operation_id.clone(),
2230 event_id.clone(),
2231 step_seq,
2232 KernelFault {
2233 code,
2234 message,
2235 operation_id: Some(operation_id),
2236 event_id: Some(event_id),
2237 effect_id,
2238 },
2239 )
2240 }
2241}
2242
2243fn normalize_input(input: &mut KernelInput) {
2244 if let KernelInputEvent::CancelOperation {
2245 pending_call_ids, ..
2246 } = &mut input.event
2247 {
2248 pending_call_ids.sort();
2249 pending_call_ids.dedup();
2250 }
2251}
2252
2253fn transaction_fault_step(
2254 operation_id: String,
2255 event_id: String,
2256 step_seq: u64,
2257 code: KernelFaultCode,
2258 message: String,
2259) -> KernelStep {
2260 KernelStep::fault(
2261 operation_id.clone(),
2262 event_id.clone(),
2263 step_seq,
2264 KernelFault {
2265 code,
2266 message,
2267 operation_id: Some(operation_id),
2268 event_id: Some(event_id),
2269 effect_id: None,
2270 },
2271 )
2272}
2273
2274fn transaction_conflict_fault(operation_id: Option<String>, message: String) -> KernelFault {
2275 KernelFault {
2276 code: KernelFaultCode::TransactionConflict,
2277 message,
2278 operation_id,
2279 event_id: None,
2280 effect_id: None,
2281 }
2282}
2283
2284fn snapshot_fault(operation_id: Option<String>, message: String) -> KernelFault {
2285 KernelFault {
2286 code: KernelFaultCode::SnapshotIncompatible,
2287 message,
2288 operation_id,
2289 event_id: None,
2290 effect_id: None,
2291 }
2292}
2293
2294fn result_effect(event: &KernelInputEvent) -> Option<(&str, PendingEffectKind)> {
2295 match event {
2296 KernelInputEvent::ProviderResult { effect_id, .. }
2297 | KernelInputEvent::ProviderError { effect_id, .. } => {
2298 Some((effect_id, PendingEffectKind::Provider))
2299 }
2300 KernelInputEvent::ToolResults { effect_id, .. } => {
2301 Some((effect_id, PendingEffectKind::Tool))
2302 }
2303 KernelInputEvent::MilestoneResult { effect_id, .. } => {
2304 Some((effect_id, PendingEffectKind::Milestone))
2305 }
2306 KernelInputEvent::ApprovalResult { effect_id, .. } => {
2307 Some((effect_id, PendingEffectKind::Approval))
2308 }
2309 KernelInputEvent::WorkflowSpawnResult { effect_id, .. } => {
2310 Some((effect_id, PendingEffectKind::WorkflowSpawn))
2311 }
2312 KernelInputEvent::PreemptResult { effect_id, .. } => {
2313 Some((effect_id, PendingEffectKind::Preempt))
2314 }
2315 KernelInputEvent::MemoryPersistResult { effect_id, .. } => {
2316 Some((effect_id, PendingEffectKind::MemoryPersist))
2317 }
2318 KernelInputEvent::MemoryQueryResult { effect_id, .. } => {
2319 Some((effect_id, PendingEffectKind::MemoryQuery))
2320 }
2321 KernelInputEvent::LargeResultSpoolResult { effect_id, .. } => {
2322 Some((effect_id, PendingEffectKind::LargeResultSpool))
2323 }
2324 KernelInputEvent::PageOutArchiveResult { effect_id, .. } => {
2325 Some((effect_id, PendingEffectKind::PageOutArchive))
2326 }
2327 _ => None,
2328 }
2329}
2330
2331fn pending_effect_kind(effect: &KernelEffect) -> Option<PendingEffectKind> {
2332 match effect {
2333 KernelEffect::CallProvider { .. } => Some(PendingEffectKind::Provider),
2334 KernelEffect::ExecuteTool { .. } => Some(PendingEffectKind::Tool),
2335 KernelEffect::EvaluateMilestone { .. } => Some(PendingEffectKind::Milestone),
2336 KernelEffect::RequestApproval { .. } => Some(PendingEffectKind::Approval),
2337 KernelEffect::SpawnWorkflow { .. } => Some(PendingEffectKind::WorkflowSpawn),
2338 KernelEffect::PreemptSubAgents { .. } => Some(PendingEffectKind::Preempt),
2339 KernelEffect::PersistMemory { .. } => Some(PendingEffectKind::MemoryPersist),
2340 KernelEffect::QueryMemory { .. } => Some(PendingEffectKind::MemoryQuery),
2341 KernelEffect::SpoolLargeResult { .. } => Some(PendingEffectKind::LargeResultSpool),
2342 KernelEffect::ArchivePageOut { .. } => Some(PendingEffectKind::PageOutArchive),
2343 KernelEffect::Done { .. } => None,
2344 }
2345}
2346
2347fn validate_run_config(config: &RunConfig, max_tokens: u32) -> Result<(), String> {
2348 if config
2349 .budget_grant
2350 .as_ref()
2351 .is_some_and(|grant| grant.reservation_id.is_empty())
2352 {
2353 return Err("budget_grant reservation_id must be non-empty".to_string());
2354 }
2355 if let Some(reliability) = &config.reliability {
2356 for (name, capacity) in [
2357 ("event_replay_capacity", reliability.event_replay_capacity),
2358 (
2359 "completed_effect_replay_capacity",
2360 reliability.completed_effect_replay_capacity,
2361 ),
2362 ] {
2363 if let Some(capacity) = capacity {
2364 if !(1..=65_536).contains(&capacity) {
2365 return Err(format!("{name} must be between 1 and 65536"));
2366 }
2367 }
2368 }
2369 if reliability
2370 .snapshot_input_limit
2371 .is_some_and(|value| !(1..=100_000).contains(&value))
2372 {
2373 return Err("snapshot_input_limit must be between 1 and 100000".to_string());
2374 }
2375 if reliability
2376 .max_input_bytes
2377 .is_some_and(|value| !(256..=64 * 1024 * 1024).contains(&value))
2378 {
2379 return Err("max_input_bytes must be between 256 and 67108864".to_string());
2380 }
2381 if reliability
2382 .snapshot_journal_bytes_limit
2383 .is_some_and(|value| !(256..=1024 * 1024 * 1024).contains(&value))
2384 {
2385 return Err(
2386 "snapshot_journal_bytes_limit must be between 256 and 1073741824".to_string(),
2387 );
2388 }
2389 if reliability
2390 .provider_recovery_attempts
2391 .is_some_and(|value| value > 16)
2392 {
2393 return Err("provider_recovery_attempts must be at most 16".to_string());
2394 }
2395 if reliability
2396 .output_recovery_attempts
2397 .is_some_and(|value| value > 16)
2398 {
2399 return Err("output_recovery_attempts must be at most 16".to_string());
2400 }
2401 if reliability
2402 .host_effect_retry_attempts
2403 .is_some_and(|value| value > 16)
2404 {
2405 return Err("host_effect_retry_attempts must be at most 16".to_string());
2406 }
2407 let threshold = reliability.spool_threshold_bytes.unwrap_or(50 * 1024);
2408 let preview = reliability.spool_preview_bytes.unwrap_or(2 * 1024);
2409 if threshold == 0 {
2410 return Err("spool_threshold_bytes must be greater than zero".to_string());
2411 }
2412 if preview == 0 || preview > threshold {
2413 return Err(
2414 "spool_preview_bytes must be greater than zero and no larger than spool_threshold_bytes"
2415 .to_string(),
2416 );
2417 }
2418 }
2419 if let Some(policy) = &config.signal_policy {
2420 validate_signal_policy(policy)?;
2421 }
2422 if config
2423 .prompt_budget
2424 .is_some_and(|budget| budget.reserved_tokens() >= max_tokens)
2425 {
2426 return Err(
2427 "prompt_budget reserves must leave at least one token for provider input".to_string(),
2428 );
2429 }
2430 if let Some(policy) = &config.context_policy {
2431 policy.validate()?;
2432 }
2433 if let Some(policy) = config.scheduler_policy {
2434 policy.validate()?;
2435 }
2436 if let Some(ratio) = config.knowledge_budget_ratio {
2437 if !ratio.is_finite() || !(0.0..=1.0).contains(&ratio) {
2438 return Err("knowledge_budget_ratio must be finite and between 0 and 1".to_string());
2439 }
2440 }
2441 if let Some(fuse) = config.repeat_fuse {
2442 if fuse.enabled
2443 && fuse.deny_after > 0
2444 && fuse.terminate_after > 0
2445 && fuse.deny_after >= fuse.terminate_after
2446 {
2447 return Err("repeat_fuse terminate_after must be greater than deny_after".to_string());
2448 }
2449 }
2450 if let Some(watch) = config.entropy_watch {
2451 if !watch.threshold.is_finite() || !(0.0..=1.0).contains(&watch.threshold) {
2452 return Err("entropy_watch threshold must be finite and between 0 and 1".to_string());
2453 }
2454 if !watch.hysteresis.is_finite()
2455 || watch.hysteresis < 0.0
2456 || watch.hysteresis > watch.threshold
2457 {
2458 return Err(
2459 "entropy_watch hysteresis must be finite and no greater than threshold".to_string(),
2460 );
2461 }
2462 }
2463 if let Some(quota) = &config.resource_quota {
2464 if matches!(quota.memory_writes_per_window, Some((_, 0))) {
2465 return Err("memory write quota window must be greater than zero".to_string());
2466 }
2467 }
2468 if let Some(governance) = &config.governance {
2469 if governance
2470 .rate_limits
2471 .iter()
2472 .any(|limit| limit.window_ms == 0)
2473 {
2474 return Err("governance rate-limit windows must be greater than zero".to_string());
2475 }
2476 for constraint in &governance.constraints {
2477 if let ConstraintSpec::Range {
2478 min: Some(min),
2479 max: Some(max),
2480 ..
2481 } = constraint
2482 {
2483 if !min.is_finite() || !max.is_finite() || min > max {
2484 return Err(
2485 "governance range constraints require finite min <= max".to_string()
2486 );
2487 }
2488 }
2489 }
2490 }
2491 Ok(())
2492}
2493
2494fn validate_signal_policy(policy: &SignalPolicyConfig) -> Result<(), String> {
2495 if policy.version != SIGNAL_POLICY_VERSION {
2496 return Err(format!(
2497 "signal_policy version must be {SIGNAL_POLICY_VERSION}"
2498 ));
2499 }
2500 if policy.queue_max == 0 {
2501 return Err("signal_policy.queue_max must be greater than zero".to_string());
2502 }
2503 if matches!(policy.ttl_ms, Some(0)) {
2504 return Err("signal_policy.ttl_ms must be greater than zero when present".to_string());
2505 }
2506 Ok(())
2507}