Skip to main content

deepstrike_core/runtime/kernel/
protocol.rs

1use super::*;
2pub const KERNEL_ABI_VERSION: u32 = 2;
3pub const KERNEL_SNAPSHOT_VERSION: u32 = 2;
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
6#[serde(rename_all = "snake_case")]
7pub enum KernelLifecycle {
8    Created,
9    Configured,
10    Running,
11    Suspended,
12    Completed,
13    Cancelled,
14    Failed,
15}
16
17impl KernelLifecycle {
18    pub fn is_terminal(self) -> bool {
19        matches!(self, Self::Completed | Self::Cancelled | Self::Failed)
20    }
21}
22
23/// Serializable permission action for the governance ABI.
24/// Mirrors [`crate::governance::permission::PermissionAction`] without coupling
25/// the wire format to the internal type.
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
27#[serde(rename_all = "snake_case")]
28pub enum PolicyAction {
29    Allow,
30    Deny,
31    AskUser,
32}
33
34impl From<PolicyAction> for crate::governance::permission::PermissionAction {
35    fn from(action: PolicyAction) -> Self {
36        match action {
37            PolicyAction::Allow => Self::Allow,
38            PolicyAction::Deny => Self::Deny,
39            PolicyAction::AskUser => Self::AskUser,
40        }
41    }
42}
43
44/// One permission rule for the governance ABI: glob `tool_pattern` → action.
45#[derive(Debug, Clone, Serialize, Deserialize)]
46pub struct PolicyRule {
47    pub tool_pattern: String,
48    pub action: PolicyAction,
49}
50
51/// Per-tool rate limit for the governance ABI.
52/// Maps to [`crate::governance::rate_limit::RateLimit`].
53#[derive(Debug, Clone, Serialize, Deserialize)]
54pub struct RateLimitSpec {
55    pub tool: String,
56    pub max_calls: u32,
57    pub window_ms: u64,
58}
59
60/// Parameter constraint for the governance ABI.
61/// Maps to [`crate::governance::constraint::ConstraintRule`] (structural rules only;
62/// pattern/predicate matching stays in the SDK execution layer).
63#[derive(Debug, Clone, Serialize, Deserialize)]
64#[serde(tag = "kind", rename_all = "snake_case")]
65pub enum ConstraintSpec {
66    /// Parameter must be present and non-null.
67    Required { tool: String, path: String },
68    /// Parameter value must be one of `values`.
69    Enum {
70        tool: String,
71        path: String,
72        values: Vec<String>,
73    },
74    /// Numeric parameter must fall within `[min, max]`.
75    Range {
76        tool: String,
77        path: String,
78        #[serde(default, skip_serializing_if = "Option::is_none")]
79        min: Option<f64>,
80        #[serde(default, skip_serializing_if = "Option::is_none")]
81        max: Option<f64>,
82    },
83}
84
85#[derive(Debug, Clone, Serialize, Deserialize)]
86pub struct KernelInput {
87    pub version: u32,
88    pub operation_id: String,
89    pub event_id: String,
90    pub observed_at_ms: u64,
91    pub event: KernelInputEvent,
92}
93
94impl KernelInput {
95    /// Operation identity used by [`Self::new`] for in-process callers. Hosts that correlate
96    /// against it (e.g. a `cancel_operation` for an operation started via `new`) must use this
97    /// constant, never a re-typed literal. NOT for durable wire hosts: anything keyed by
98    /// `(session, operation)` in storage that outlives the process needs a unique identity via
99    /// [`Self::correlated`].
100    pub const LOCAL_OPERATION_ID: &'static str = "local-operation";
101
102    /// Build an in-process input for callers that do not cross a durable wire boundary.
103    /// Wire hosts should use [`Self::correlated`] with their durable identities.
104    pub fn new(event: KernelInputEvent) -> Self {
105        use std::sync::atomic::{AtomicU64, Ordering};
106        static LOCAL_EVENT_SEQUENCE: AtomicU64 = AtomicU64::new(1);
107        let event_seq = LOCAL_EVENT_SEQUENCE.fetch_add(1, Ordering::Relaxed);
108        Self::correlated(
109            Self::LOCAL_OPERATION_ID,
110            format!("local-event-{event_seq}"),
111            0,
112            event,
113        )
114    }
115
116    pub fn correlated(
117        operation_id: impl Into<String>,
118        event_id: impl Into<String>,
119        observed_at_ms: u64,
120        event: KernelInputEvent,
121    ) -> Self {
122        Self {
123            version: KERNEL_ABI_VERSION,
124            operation_id: operation_id.into(),
125            event_id: event_id.into(),
126            observed_at_ms,
127            event,
128        }
129    }
130}
131
132/// Outcome of staging one kernel input before the host's durable commit boundary.
133#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
134#[serde(rename_all = "snake_case")]
135pub enum KernelPreparationStatus {
136    /// A new accepted transition is staged and must be committed or aborted with `prepare_token`.
137    Prepared,
138    /// The exact event was already committed; no new durable transaction is required.
139    Replayed,
140    /// The input was rejected and did not stage any runtime state.
141    Rejected,
142}
143
144/// Host-visible description of a staged transition. The candidate runtime state remains opaque
145/// inside [`KernelRuntime`](super::KernelRuntime); hosts persist `input` plus `step` before using the
146/// one-shot token to publish it.
147#[derive(Debug, Clone, Serialize, Deserialize)]
148pub struct KernelPreparedStep {
149    pub status: KernelPreparationStatus,
150    /// Committed runtime generation used to plan this outcome.
151    pub base_generation: u64,
152    #[serde(default, skip_serializing_if = "Option::is_none")]
153    pub prepare_token: Option<String>,
154    pub input: KernelInput,
155    pub step: KernelStep,
156}
157
158/// K2: the governance sub-bundle of [`RunConfig`] — the same five fields as the `LoadGovernancePolicy`
159/// event, grouped so a run's whole governance posture travels as one value.
160#[derive(Debug, Clone, Default, Serialize, Deserialize)]
161pub struct GovernanceConfig {
162    #[serde(default, skip_serializing_if = "Option::is_none")]
163    pub default_action: Option<PolicyAction>,
164    #[serde(default, skip_serializing_if = "Vec::is_empty")]
165    pub rules: Vec<PolicyRule>,
166    #[serde(default, skip_serializing_if = "Vec::is_empty")]
167    pub vetoed_tools: Vec<String>,
168    #[serde(default, skip_serializing_if = "Vec::is_empty")]
169    pub rate_limits: Vec<RateLimitSpec>,
170    #[serde(default, skip_serializing_if = "Vec::is_empty")]
171    pub constraints: Vec<ConstraintSpec>,
172}
173
174pub const SIGNAL_POLICY_VERSION: u32 = 1;
175
176/// Versioned, atomically validated signal-routing policy.
177#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
178#[serde(deny_unknown_fields)]
179pub struct SignalPolicyConfig {
180    pub version: u32,
181    pub queue_max: u32,
182    #[serde(default, skip_serializing_if = "Option::is_none")]
183    pub ttl_ms: Option<u64>,
184    #[serde(default, skip_serializing_if = "Option::is_none")]
185    pub deadline_escalation: Option<bool>,
186}
187
188/// Host-selectable reliability policy. These values bound retained replay
189/// state and retry ladders; omitted fields keep the kernel defaults.
190#[derive(Debug, Clone, Default, Serialize, Deserialize)]
191pub struct KernelReliabilityConfig {
192    #[serde(default, skip_serializing_if = "Option::is_none")]
193    pub event_replay_capacity: Option<usize>,
194    #[serde(default, skip_serializing_if = "Option::is_none")]
195    pub completed_effect_replay_capacity: Option<usize>,
196    #[serde(default, skip_serializing_if = "Option::is_none")]
197    pub provider_recovery_attempts: Option<u8>,
198    #[serde(default, skip_serializing_if = "Option::is_none")]
199    pub output_recovery_attempts: Option<u8>,
200    #[serde(default, skip_serializing_if = "Option::is_none")]
201    pub host_effect_retry_attempts: Option<u8>,
202    #[serde(default, skip_serializing_if = "Option::is_none")]
203    pub spool_threshold_bytes: Option<u32>,
204    #[serde(default, skip_serializing_if = "Option::is_none")]
205    pub spool_preview_bytes: Option<u32>,
206    /// Maximum accepted ABI inputs retained for a portable snapshot rebuild.
207    #[serde(default, skip_serializing_if = "Option::is_none")]
208    pub snapshot_input_limit: Option<usize>,
209    /// Maximum canonical JSON bytes accepted for one ABI input.
210    #[serde(default, skip_serializing_if = "Option::is_none")]
211    pub max_input_bytes: Option<usize>,
212    /// Maximum canonical JSON bytes retained by the portable snapshot journal.
213    #[serde(default, skip_serializing_if = "Option::is_none")]
214    pub snapshot_journal_bytes_limit: Option<usize>,
215}
216
217/// Read-only runtime resource projection. Hosts use this for admission and monitoring; mutating
218/// kernel state still requires a versioned input transaction.
219#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
220pub struct KernelDiagnostics {
221    pub lifecycle: KernelLifecycle,
222    pub next_step_seq: u64,
223    pub accepted_input_count: usize,
224    pub accepted_input_bytes: usize,
225    pub snapshot_input_limit: usize,
226    pub snapshot_journal_bytes_limit: usize,
227    pub max_input_bytes: usize,
228    pub snapshot_overflowed: bool,
229    pub recorded_event_count: usize,
230    pub completed_effect_count: usize,
231    pub pending_effect_count: usize,
232}
233
234/// Portable runtime checkpoint. State is rebuilt from accepted public ABI transactions rather
235/// than serializing private scheduler structs, so internal refactors do not change this schema.
236#[derive(Debug, Clone, Serialize, Deserialize)]
237pub struct KernelSnapshot {
238    pub snapshot_version: u32,
239    pub abi_version: u32,
240    pub initial_policy: KernelSnapshotPolicy,
241    pub lifecycle: KernelLifecycle,
242    #[serde(default, skip_serializing_if = "Option::is_none")]
243    pub operation_id: Option<String>,
244    pub next_step_seq: u64,
245    pub snapshot_input_limit: usize,
246    pub max_input_bytes: usize,
247    pub snapshot_journal_bytes_limit: usize,
248    pub accepted_input_bytes: usize,
249    #[serde(default)]
250    pub accepted_inputs: Vec<KernelInput>,
251    #[serde(default, skip_serializing_if = "Option::is_none")]
252    pub last_step: Option<KernelStep>,
253}
254
255/// JSON-portable scheduler policy. The 64-bit axes use decimal strings so JavaScript hosts do not
256/// lose precision while parsing and re-encoding a checkpoint.
257#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
258pub struct KernelSnapshotPolicy {
259    pub max_tokens: u32,
260    pub max_turns: u32,
261    pub max_total_tokens: String,
262    #[serde(default, skip_serializing_if = "Option::is_none")]
263    pub max_wall_ms: Option<String>,
264}
265
266impl From<&SchedulerBudget> for KernelSnapshotPolicy {
267    fn from(policy: &SchedulerBudget) -> Self {
268        Self {
269            max_tokens: policy.max_tokens,
270            max_turns: policy.max_turns,
271            max_total_tokens: policy.max_total_tokens.to_string(),
272            max_wall_ms: policy.max_wall_ms.map(|value| value.to_string()),
273        }
274    }
275}
276
277impl TryFrom<&KernelSnapshotPolicy> for SchedulerBudget {
278    type Error = String;
279
280    fn try_from(policy: &KernelSnapshotPolicy) -> Result<Self, Self::Error> {
281        Ok(Self {
282            max_tokens: policy.max_tokens,
283            max_turns: policy.max_turns,
284            max_total_tokens: policy.max_total_tokens.parse().map_err(|_| {
285                "snapshot max_total_tokens must be a u64 decimal string".to_string()
286            })?,
287            max_wall_ms: policy
288                .max_wall_ms
289                .as_deref()
290                .map(str::parse)
291                .transpose()
292                .map_err(|_| "snapshot max_wall_ms must be a u64 decimal string".to_string())?,
293        })
294    }
295}
296
297/// K2: a bundle of run-setup configuration carried by the [`KernelInputEvent::ConfigureRun`] event.
298/// Each field maps 1:1 to a granular `Set*` / `Load*` event; `None`/absent leaves that aspect untouched.
299/// This is the host-side analogue of the SDK's `applyKernelPolicies` — one event for the whole setup.
300#[derive(Debug, Clone, Default, Serialize, Deserialize)]
301#[serde(deny_unknown_fields)]
302pub struct RunConfig {
303    #[serde(default, skip_serializing_if = "Option::is_none")]
304    pub reliability: Option<KernelReliabilityConfig>,
305    #[serde(default, skip_serializing_if = "Option::is_none")]
306    pub tools: Option<Vec<ToolSchema>>,
307    #[serde(default, skip_serializing_if = "Option::is_none")]
308    pub available_skills: Option<Vec<SkillMetadata>>,
309    #[serde(default, skip_serializing_if = "Option::is_none")]
310    pub stable_core_tools: Option<Vec<String>>,
311    #[serde(default, skip_serializing_if = "Option::is_none")]
312    pub memory_enabled: Option<bool>,
313    #[serde(default, skip_serializing_if = "Option::is_none")]
314    pub knowledge_enabled: Option<bool>,
315    #[serde(default, skip_serializing_if = "Option::is_none")]
316    pub plan_tool_enabled: Option<bool>,
317    /// Present (any value) ⇒ reset the token engine to the char-approx estimator (see `SetTokenizer`).
318    #[serde(default, skip_serializing_if = "Option::is_none")]
319    pub tokenizer: Option<String>,
320    #[serde(default, skip_serializing_if = "Option::is_none")]
321    pub governance: Option<GovernanceConfig>,
322    #[serde(default, skip_serializing_if = "Option::is_none")]
323    pub signal_policy: Option<SignalPolicyConfig>,
324    /// Host-counted provider request overhead and hard output/safety reserves. These journaled
325    /// facts are deducted before the kernel renders any prompt content.
326    #[serde(default, skip_serializing_if = "Option::is_none")]
327    pub prompt_budget: Option<crate::context::config::PromptBudgetConfig>,
328    /// Stable, replayable context behavior. Ratios use integer ppm on the ABI wire.
329    #[serde(default, skip_serializing_if = "Option::is_none")]
330    pub context_policy: Option<crate::context::policy::ContextPolicyV1>,
331    #[serde(default, skip_serializing_if = "Option::is_none")]
332    pub scheduler_policy: Option<crate::scheduler::policy::SchedulerPolicyConfig>,
333    #[serde(default, skip_serializing_if = "Option::is_none")]
334    pub resource_quota: Option<crate::governance::quota::ResourceQuota>,
335    /// RunGroup admission result. The kernel enforces these as local hard limits and reports
336    /// terminal usage against the same opaque reservation identity.
337    #[serde(default, skip_serializing_if = "Option::is_none")]
338    pub budget_grant: Option<BudgetGrant>,
339    /// O6: repeat-fuse thresholds (see `SetRepeatFuse`). Absent ⇒ kernel defaults
340    /// (enabled, deny_after=5, terminate_after=8).
341    #[serde(default, skip_serializing_if = "Option::is_none")]
342    pub repeat_fuse: Option<crate::governance::repeat_fuse::RepeatFuseConfig>,
343    /// O4: enable/disable the turn-end criteria gate. Absent ⇒ enabled (kernel default).
344    #[serde(default, skip_serializing_if = "Option::is_none")]
345    pub criteria_gate: Option<bool>,
346    /// P1 fail-closed dispatch: `"exposed"` (kernel default) executes only tools this run actually
347    /// advertised to the model — anything else commits a visible denied result instead of running.
348    /// `"registered"` is the escape hatch that restores the permissive pre-gate behavior (any
349    /// registered tool the model names executes). Absent ⇒ `"exposed"`.
350    #[serde(default, skip_serializing_if = "Option::is_none")]
351    pub tool_dispatch_gate: Option<String>,
352    /// K2: max share of `max_tokens` the knowledge partition may occupy (see
353    /// `ContextConfig::knowledge_budget_ratio`). Absent ⇒ kernel default (0.25); `0.0` disables.
354    #[serde(default, skip_serializing_if = "Option::is_none")]
355    pub knowledge_budget_ratio: Option<f64>,
356    /// Entropy watch: opt-in threshold alerting over the per-turn session-entropy score
357    /// (see `SetEntropyWatch`). Absent ⇒ kernel default (disabled; sampling itself is
358    /// unconditional and unaffected).
359    #[serde(default, skip_serializing_if = "Option::is_none")]
360    pub entropy_watch: Option<crate::scheduler::entropy::EntropyWatchConfig>,
361}
362
363#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
364pub struct BudgetGrant {
365    pub reservation_id: String,
366    #[serde(default, skip_serializing_if = "Option::is_none")]
367    pub tokens: Option<u64>,
368    #[serde(default, skip_serializing_if = "Option::is_none")]
369    pub subagents: Option<u32>,
370    #[serde(default, skip_serializing_if = "Option::is_none")]
371    pub rounds: Option<u32>,
372}
373
374#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
375#[serde(rename_all = "snake_case")]
376pub enum CancellationReason {
377    User,
378    Deadline,
379    LeaseLost,
380    HostShutdown,
381}
382
383/// Build a [`GovernancePipeline`](crate::governance::pipeline::GovernancePipeline) from the ABI policy
384/// fields. Shared by the `LoadGovernancePolicy` event and the `ConfigureRun` bundle so the two can never
385/// drift in how they interpret rules / vetoes / rate-limits / constraints.
386pub(crate) fn build_governance_pipeline(
387    default_action: Option<PolicyAction>,
388    rules: Vec<PolicyRule>,
389    vetoed_tools: Vec<String>,
390    rate_limits: Vec<RateLimitSpec>,
391    constraints: Vec<ConstraintSpec>,
392) -> crate::governance::pipeline::GovernancePipeline {
393    use crate::governance::constraint::{ConstraintRule, ParamConstraint};
394    use crate::governance::permission::PermissionRule;
395    use crate::governance::rate_limit::RateLimit;
396    let default = default_action.unwrap_or(PolicyAction::Allow).into();
397    let mut pipeline = crate::governance::pipeline::GovernancePipeline::new(default);
398    for rule in rules {
399        pipeline.permission.add_rule(PermissionRule {
400            tool_pattern: rule.tool_pattern.into(),
401            action: rule.action.into(),
402        });
403    }
404    for tool in vetoed_tools {
405        pipeline.veto.block_tool(tool);
406    }
407    for rl in rate_limits {
408        pipeline.rate_limiter.set_limit(
409            rl.tool,
410            RateLimit {
411                max_calls: rl.max_calls,
412                window_ms: rl.window_ms,
413            },
414        );
415    }
416    for c in constraints {
417        let (tool_name, param_path, rule) = match c {
418            ConstraintSpec::Required { tool, path } => (tool, path, ConstraintRule::Required),
419            ConstraintSpec::Enum { tool, path, values } => {
420                (tool, path, ConstraintRule::Enum(values))
421            }
422            ConstraintSpec::Range {
423                tool,
424                path,
425                min,
426                max,
427            } => (tool, path, ConstraintRule::Range { min, max }),
428        };
429        pipeline.constraints.add(ParamConstraint {
430            tool_name,
431            param_path,
432            rule,
433        });
434    }
435    pipeline
436}
437
438#[derive(Debug, Clone, Serialize, Deserialize)]
439#[serde(tag = "kind", rename_all = "snake_case")]
440pub enum KernelInputEvent {
441    SetTools {
442        tools: Vec<ToolSchema>,
443    },
444    SetAvailableSkills {
445        skills: Vec<SkillMetadata>,
446    },
447    /// P1-B tool gating: the model loaded a skill (`name`). The SDK emits this when it resolves a
448    /// `skill` tool call. The kernel records it in the active-skill set and resolves the skill's
449    /// `allowed_tools` from the catalog to narrow the toolset on subsequent turns.
450    SkillActivated {
451        name: String,
452        /// K3: auto-deactivate after this many turns (`None` = permanent, the default). On expiry
453        /// the toolset re-widens and the skill's knowledge pin is boundary-swept — same path as
454        /// an explicit `SkillDeactivated`.
455        #[serde(default, skip_serializing_if = "Option::is_none")]
456        lease_turns: Option<u32>,
457    },
458    /// K3: host-driven skill deactivation (there is deliberately NO model-facing unload — it
459    /// invites thrash). The toolset re-widens at the next provider call (an epoch event, same
460    /// cache cost class as activation); the `skill:<name>` knowledge pin drops at the next
461    /// boundary sweep. Errs-open: not-active is a no-op.
462    SkillDeactivated {
463        name: String,
464    },
465    /// P1-B/D: configure the stable-core tool ids (always exposed under skill gating). Set once by
466    /// the SDK; empty/absent ⇒ skills narrow to exactly their declared tools + meta-tools.
467    SetStableCoreTools {
468        tool_ids: Vec<String>,
469    },
470    SetMemoryEnabled {
471        enabled: bool,
472    },
473    SetKnowledgeEnabled {
474        enabled: bool,
475    },
476    SetPlanToolEnabled {
477        enabled: bool,
478    },
479    SetTokenizer {
480        name: String,
481    },
482    AddSystemMessage {
483        content: String,
484        tokens: u32,
485    },
486    AddKnowledgeMessage {
487        content: String,
488        tokens: u32,
489        /// K1: entry identity. `Some` ⇒ upsert semantics (same key replaces at the next
490        /// boundary); `None` ⇒ legacy unkeyed append. Additive — old logs replay unchanged.
491        #[serde(default, skip_serializing_if = "Option::is_none")]
492        key: Option<String>,
493        /// K1: host-pinned entries are exempt from the K2 budget sweep.
494        #[serde(default, skip_serializing_if = "std::ops::Not::not")]
495        pinned: bool,
496    },
497    /// K1: mark a keyed knowledge entry for removal at the next compaction/renewal boundary.
498    /// Errs-open: unknown key is a no-op.
499    RemoveKnowledge {
500        key: String,
501    },
502    AddHistoryMessage {
503        message: Message,
504        tokens: Option<u32>,
505    },
506    PreloadHistory {
507        messages: Vec<Message>,
508    },
509    MountCapability {
510        capability: CapabilityDescriptor,
511    },
512    UnmountCapability {
513        capability_kind: CapabilityKind,
514        id: String,
515    },
516    LoadMilestoneContract {
517        contract: MilestoneContract,
518    },
519    /// Install a governance policy. Once loaded, every model-proposed tool call
520    /// is evaluated in-kernel before execution. Omitting this event leaves the
521    /// gate disabled (pre-governance behavior).
522    LoadGovernancePolicy {
523        #[serde(default)]
524        default_action: Option<PolicyAction>,
525        #[serde(default, skip_serializing_if = "Vec::is_empty")]
526        rules: Vec<PolicyRule>,
527        #[serde(default, skip_serializing_if = "Vec::is_empty")]
528        vetoed_tools: Vec<String>,
529        // COMPAT(gov-abi-additive): rate_limits/constraints are additive fields with
530        // serde(default) so older SDKs that omit them still deserialize. Safe to keep.
531        #[serde(default, skip_serializing_if = "Vec::is_empty")]
532        rate_limits: Vec<RateLimitSpec>,
533        #[serde(default, skip_serializing_if = "Vec::is_empty")]
534        constraints: Vec<ConstraintSpec>,
535    },
536    /// Atomically replace the complete signal-routing policy.
537    SetSignalPolicy {
538        policy: SignalPolicyConfig,
539    },
540    ForceCompact,
541    UpdateTask {
542        update: TaskUpdate,
543    },
544    StartRun {
545        task: RuntimeTask,
546        #[serde(default, skip_serializing_if = "Option::is_none")]
547        run_spec: Option<AgentRunSpec>,
548    },
549    /// K2: apply a bundle of run-setup configuration in a single event. Every field is optional;
550    /// an absent field leaves that aspect untouched. This is a strict single-version contract:
551    /// unknown and superseded policy fields are rejected rather than silently ignored.
552    ConfigureRun {
553        config: RunConfig,
554    },
555    CapabilityCommand {
556        command: CapabilityCommand,
557    },
558    /// Continue a run reconstructed from preloaded history. Approval resolution
559    /// uses the correlated `ApprovalResult` event instead.
560    Resume,
561    ApprovalResult {
562        effect_id: String,
563        #[serde(default, skip_serializing_if = "Vec::is_empty")]
564        approved_calls: Vec<String>,
565        #[serde(default, skip_serializing_if = "Vec::is_empty")]
566        denied_calls: Vec<String>,
567        #[serde(default, skip_serializing_if = "Option::is_none")]
568        error: Option<String>,
569    },
570    /// Result of a host-owned workflow spawn batch. Every requested agent must
571    /// appear in exactly one of `started_agent_ids` or `failures`.
572    WorkflowSpawnResult {
573        effect_id: String,
574        #[serde(default, skip_serializing_if = "Vec::is_empty")]
575        started_agent_ids: Vec<String>,
576        #[serde(default, skip_serializing_if = "Vec::is_empty")]
577        failures: Vec<WorkflowSpawnFailure>,
578        #[serde(default, skip_serializing_if = "Option::is_none")]
579        error: Option<String>,
580    },
581    PreemptResult {
582        effect_id: String,
583        #[serde(default, skip_serializing_if = "Option::is_none")]
584        error: Option<String>,
585    },
586    MemoryPersistResult {
587        effect_id: String,
588        #[serde(default, skip_serializing_if = "Option::is_none")]
589        error: Option<String>,
590    },
591    MemoryQueryResult {
592        effect_id: String,
593        #[serde(default, skip_serializing_if = "Vec::is_empty")]
594        hits: Vec<crate::mm::memory::MemoryRecall>,
595        #[serde(default, skip_serializing_if = "Option::is_none")]
596        error: Option<String>,
597    },
598    LargeResultSpoolResult {
599        effect_id: String,
600        #[serde(default, skip_serializing_if = "Option::is_none")]
601        spool_ref: Option<String>,
602        #[serde(default, skip_serializing_if = "Option::is_none")]
603        error: Option<String>,
604    },
605    PageOutArchiveResult {
606        effect_id: String,
607        #[serde(default, skip_serializing_if = "Option::is_none")]
608        archive_ref: Option<String>,
609        #[serde(default, skip_serializing_if = "Option::is_none")]
610        error: Option<String>,
611    },
612    /// K2: set the knowledge-budget ratio at runtime (granular sibling of
613    /// `RunConfig::knowledge_budget_ratio`). `0.0` disables the cap.
614    SetKnowledgeBudget {
615        ratio: f64,
616    },
617    /// O4: enable/disable the turn-end criteria gate (default enabled; no-op for runs without
618    /// criteria). Additive ABI.
619    SetCriteriaGate {
620        enabled: bool,
621    },
622    /// O6: tune or disable the repeat fuse (defaults: enabled, deny_after=5, terminate_after=8).
623    /// Each field is optional — an absent field keeps the current value. Additive ABI.
624    SetRepeatFuse {
625        #[serde(default, skip_serializing_if = "Option::is_none")]
626        enabled: Option<bool>,
627        #[serde(default, skip_serializing_if = "Option::is_none")]
628        deny_after: Option<u32>,
629        #[serde(default, skip_serializing_if = "Option::is_none")]
630        terminate_after: Option<u32>,
631    },
632    /// Entropy watch: tune the opt-in threshold alerting over the per-turn session-entropy
633    /// score (defaults: disabled, threshold=0.65, hysteresis=0.1, cooldown_turns=4,
634    /// notify_model=false). Each field is optional — an absent field keeps the current
635    /// value. Sampling itself is unconditional and unaffected. Additive ABI.
636    SetEntropyWatch {
637        #[serde(default, skip_serializing_if = "Option::is_none")]
638        enabled: Option<bool>,
639        #[serde(default, skip_serializing_if = "Option::is_none")]
640        threshold: Option<f64>,
641        #[serde(default, skip_serializing_if = "Option::is_none")]
642        hysteresis: Option<f64>,
643        #[serde(default, skip_serializing_if = "Option::is_none")]
644        cooldown_turns: Option<u32>,
645        #[serde(default, skip_serializing_if = "Option::is_none")]
646        notify_model: Option<bool>,
647    },
648    /// Adjust the wall-clock budget at runtime (e.g. to extend or set a deadline
649    /// after a run has already started). Additive: omit to keep the value from
650    /// `SchedulerBudget` passed at construction.
651    SetSchedulerBudget {
652        #[serde(default, skip_serializing_if = "Option::is_none")]
653        max_wall_ms: Option<u64>,
654    },
655    /// M2 资源配额: install a declarative [`crate::governance::quota::ResourceQuota`] at the
656    /// single syscall trap. Like governance/attention/scheduler config, quotas flow in through
657    /// the versioned JSON event ABI (replayable, session-loggable) rather than a side-channel
658    /// setter — sending it is opt-in, and omitting it preserves the pre-M2 unconditional `Allow`
659    /// for spawn / memory-write syscalls.
660    SetResourceQuota {
661        quota: crate::governance::quota::ResourceQuota,
662    },
663    ProviderResult {
664        effect_id: String,
665        message: Message,
666        #[serde(default, skip_serializing_if = "Option::is_none")]
667        observed_input_tokens: Option<u32>,
668        #[serde(default, skip_serializing_if = "Option::is_none")]
669        observed_output_tokens: Option<u32>,
670        // COMPAT(gov-clock): now_ms is optional so SDKs that don't drive the in-kernel
671        // governance gate need not supply a clock. When absent, the rate limiter runs
672        // on a 0 clock (effectively unlimited). Can become required once all SDKs feed time.
673        #[serde(default, skip_serializing_if = "Option::is_none")]
674        now_ms: Option<u64>,
675        /// Provider stop_reason for this response — `max_tokens` (Anthropic) / `length` (OpenAI)
676        /// signal an output-cap truncation, which drives the kernel's max-output-tokens recovery.
677        /// Additive: omitted by providers/SDKs that don't report it (no-op recovery).
678        #[serde(default, skip_serializing_if = "Option::is_none")]
679        stop_reason: Option<String>,
680    },
681    ToolResults {
682        effect_id: String,
683        results: Vec<ToolResult>,
684    },
685    /// Reactive recovery entry point: the SDK's provider stream failed. The kernel classifies the
686    /// error (context-overflow vs other) and runs the bounded compact-and-retry recovery ladder,
687    /// returning `CallProvider` to retry with a freshly compacted context or `Done` to terminate.
688    /// The runners forward the raw provider error text and dispatch the result, instead of each
689    /// owning the classify + compact + retry + give-up policy. Additive ABI: a brand-new variant,
690    /// byte-identical on the wire for SDKs that never send it.
691    ProviderError {
692        effect_id: String,
693        message: String,
694    },
695    DeliverSignal {
696        delivery_id: String,
697        attempt: u32,
698        signal: RuntimeSignal,
699    },
700    MilestoneResult {
701        effect_id: String,
702        result: MilestoneCheckResult,
703    },
704    /// Spawn a sub-agent: registers/updates the kernel process table.
705    SpawnSubAgent {
706        spec: AgentRunSpec,
707        parent_session_id: String,
708    },
709    /// W0-ABI: load a workflow DAG and spawn its first gated batch. The kernel drives the DAG;
710    /// each node spawn passes the syscall trap and is reported via `workflow_batch_spawned`.
711    /// Completions feed back through `SubAgentCompleted` (reused); finish emits
712    /// `workflow_completed`.
713    LoadWorkflow {
714        spec: crate::orchestration::workflow::WorkflowSpec,
715        parent_session_id: String,
716        /// R3-1 resume: the runtime `submit_workflow_nodes` batches (in order) recovered from the log,
717        /// re-applied before completions so dynamically-appended nodes are reconstructed. Additive:
718        /// empty for a fresh run or a resume without dynamic submissions.
719        #[serde(default, skip_serializing_if = "Vec::is_empty")]
720        resumed_submissions: Vec<Vec<crate::orchestration::workflow::WorkflowNode>>,
721        /// Exact base graph index for every recovered submission batch. Length must equal
722        /// `resumed_submissions`; mismatch rejects the resume atomically.
723        #[serde(default)]
724        resumed_submission_bases: Vec<u32>,
725        /// Typed recovered terminal outcomes plus control signals. Status, termination and output
726        /// are mandatory facts for exact dependency-policy replay; bare completed ids are invalid.
727        #[serde(default, skip_serializing_if = "Vec::is_empty")]
728        resumed_outcomes: Vec<crate::orchestration::workflow::ResumedNodeOutcome>,
729    },
730    /// Feed a completed sub-agent result back into the parent loop.
731    SubAgentCompleted {
732        result: SubAgentResult,
733    },
734    /// R3-1: append nodes to the in-flight workflow DAG at runtime (dynamic fan-out /
735    /// loop-until-done). Sent by the SDK while the submitting node is still running — the appended
736    /// nodes spawn on the next gated drive. No-op if no workflow is active. Additive ABI: a brand-new
737    /// event variant, so existing SDKs that never send it are byte-identical on the wire.
738    SubmitWorkflowNodes {
739        #[serde(default, skip_serializing_if = "Vec::is_empty")]
740        nodes: Vec<crate::orchestration::workflow::WorkflowNode>,
741        /// G1: the agent id of the node that requested this submission. When it names a quarantined
742        /// node, the kernel coerces every submitted node to quarantined (no privilege escalation
743        /// across the trust boundary). Additive: omitted by older SDKs → `None` → no coercion.
744        #[serde(default, skip_serializing_if = "Option::is_none")]
745        submitter_agent_id: Option<String>,
746    },
747    /// M5/G1: an agent authors a whole `WorkflowSpec` (the article's "model writes its own harness").
748    /// The agent-reachable analogue of the host-only `LoadWorkflow`: **bootstraps** the DAG when no
749    /// workflow is active, else **flattens** the spec's nodes onto the running DAG (bootstrap-or-flatten,
750    /// one kernel / one quota — never a workflow stack). Gated by `Syscall::LoadWorkflow`. Additive ABI:
751    /// a brand-new variant, byte-identical on the wire for SDKs that never send it.
752    SubmitWorkflow {
753        spec: crate::orchestration::workflow::WorkflowSpec,
754        /// Used only on bootstrap (no workflow active) to seed child session ids; ignored on flatten.
755        #[serde(default)]
756        parent_session_id: String,
757        /// G1: the authoring node's agent id (flatten case) — a quarantined author's nodes are coerced
758        /// quarantined. Additive: omitted (top-level bootstrap) → `None` → the run's own trust applies.
759        #[serde(default, skip_serializing_if = "Option::is_none")]
760        submitter_agent_id: Option<String>,
761    },
762    /// Feed long-term memory entries into the knowledge partition (page-in).
763    /// SDK performs retrieval I/O; kernel only applies the result.
764    PageIn {
765        #[serde(default, skip_serializing_if = "Vec::is_empty")]
766        entries: Vec<crate::mm::PageInEntry>,
767    },
768    /// Configure long-term memory management policy (Phase 7). Opt-in: installing the policy makes
769    /// `validation_enabled`, `retrieval_top_k`, and the optional size/name overrides authoritative.
770    SetMemoryPolicy {
771        #[serde(default)]
772        memory_path: String,
773        #[serde(default = "default_stale_days")]
774        stale_warning_days: u32,
775        #[serde(default = "default_top_k")]
776        retrieval_top_k: usize,
777        #[serde(default = "default_validation_enabled")]
778        validation_enabled: bool,
779        /// Override the validation content-size limit (bytes). Omit to keep the kernel default.
780        #[serde(default, skip_serializing_if = "Option::is_none")]
781        max_content_bytes: Option<u32>,
782        /// Override the validation name-length limit. Omit to keep the kernel default.
783        #[serde(default, skip_serializing_if = "Option::is_none")]
784        max_name_length: Option<usize>,
785        /// M4: recall count at which a record becomes a promotion candidate. Omit to disable.
786        #[serde(default, skip_serializing_if = "Option::is_none")]
787        promotion_recall_threshold: Option<u64>,
788    },
789    /// Write a long-term memory entry (SDK background agent calls this).
790    WriteMemory {
791        memory: crate::mm::memory::MemoryRecord,
792    },
793    /// Query long-term memory for context (kernel calls this; SDK responds asynchronously).
794    QueryMemory {
795        query: crate::mm::memory::MemoryQuery,
796    },
797    /// Privileged host control: commit a host-driven run (for example a standalone workflow)
798    /// after its kernel-owned work has completed. This supersedes any pending provider effect and
799    /// produces the ordinary `done` effect and terminal usage report.
800    CompleteRun,
801    /// Host cancellation fact. The host has already stopped external I/O; the kernel commits the
802    /// deterministic terminal transition and clears every pending effect/wait state.
803    CancelOperation {
804        operation_id: String,
805        reason: CancellationReason,
806        #[serde(default, skip_serializing_if = "Vec::is_empty")]
807        pending_call_ids: Vec<String>,
808    },
809}
810
811fn default_stale_days() -> u32 {
812    2
813}
814fn default_top_k() -> usize {
815    5
816}
817fn default_validation_enabled() -> bool {
818    true
819}
820
821#[derive(Debug, Clone, Serialize, Deserialize)]
822pub struct KernelStep {
823    pub version: u32,
824    pub operation_id: String,
825    pub input_event_id: String,
826    pub step_seq: u64,
827    pub actions: Vec<KernelAction>,
828    pub observations: Vec<KernelObservation>,
829    #[serde(default, skip_serializing_if = "Vec::is_empty")]
830    pub faults: Vec<KernelFault>,
831}
832
833impl KernelStep {
834    pub(super) fn empty(
835        operation_id: String,
836        input_event_id: String,
837        step_seq: u64,
838        observations: Vec<KernelObservation>,
839    ) -> Self {
840        Self {
841            version: KERNEL_ABI_VERSION,
842            operation_id,
843            input_event_id,
844            step_seq,
845            actions: Vec::new(),
846            observations,
847            faults: Vec::new(),
848        }
849    }
850
851    pub(super) fn single(
852        operation_id: String,
853        input_event_id: String,
854        step_seq: u64,
855        action: LoopAction,
856        observations: Vec<KernelObservation>,
857    ) -> Self {
858        let effect_id = format!("{operation_id}:step:{step_seq}:effect:0");
859        Self {
860            version: KERNEL_ABI_VERSION,
861            operation_id,
862            input_event_id: input_event_id.clone(),
863            step_seq,
864            actions: vec![KernelAction::from_loop(effect_id, input_event_id, action)],
865            observations,
866            faults: Vec::new(),
867        }
868    }
869
870    pub(super) fn fault(
871        operation_id: String,
872        input_event_id: String,
873        step_seq: u64,
874        fault: KernelFault,
875    ) -> Self {
876        Self {
877            version: KERNEL_ABI_VERSION,
878            operation_id,
879            input_event_id,
880            step_seq,
881            actions: Vec::new(),
882            observations: Vec::new(),
883            faults: vec![fault],
884        }
885    }
886}
887
888#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
889#[serde(rename_all = "snake_case")]
890pub enum KernelFaultCode {
891    VersionMismatch,
892    OperationMismatch,
893    InvalidLifecycle,
894    InvalidConfig,
895    ResourceLimitExceeded,
896    DuplicateEventConflict,
897    UnexpectedEffectResult,
898    TransactionConflict,
899    SnapshotIncompatible,
900}
901
902#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
903pub struct KernelFault {
904    pub code: KernelFaultCode,
905    pub message: String,
906    #[serde(default, skip_serializing_if = "Option::is_none")]
907    pub operation_id: Option<String>,
908    #[serde(default, skip_serializing_if = "Option::is_none")]
909    pub event_id: Option<String>,
910    #[serde(default, skip_serializing_if = "Option::is_none")]
911    pub effect_id: Option<String>,
912}
913
914#[derive(Debug, Clone, Serialize, Deserialize)]
915pub struct KernelAction {
916    pub effect_id: String,
917    pub causation_id: String,
918    #[serde(flatten)]
919    pub effect: KernelEffect,
920}
921
922impl KernelAction {
923    fn from_loop(effect_id: String, causation_id: String, action: LoopAction) -> Self {
924        Self {
925            effect_id,
926            causation_id,
927            effect: action.into(),
928        }
929    }
930}
931
932#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
933pub struct WorkflowSpawnFailure {
934    pub agent_id: String,
935    pub error: String,
936}
937
938#[derive(Debug, Clone, Serialize, Deserialize)]
939#[serde(tag = "kind", rename_all = "snake_case")]
940pub enum KernelEffect {
941    CallProvider {
942        context: RenderedContext,
943        tools: Vec<ToolSchema>,
944    },
945    ExecuteTool {
946        calls: Vec<ToolCall>,
947    },
948    RequestApproval {
949        requests: Vec<crate::scheduler::state_machine::ApprovalRequest>,
950    },
951    SpawnWorkflow {
952        nodes: Vec<crate::orchestration::workflow::WorkflowSpawnInfo>,
953        #[serde(default, skip_serializing_if = "Option::is_none")]
954        budget: Option<crate::orchestration::workflow::WorkflowBudget>,
955    },
956    PreemptSubAgents {
957        agent_ids: Vec<String>,
958        reason: String,
959    },
960    PersistMemory {
961        memory: crate::mm::memory::MemoryRecord,
962    },
963    QueryMemory {
964        query: crate::mm::memory::MemoryQuery,
965        requested_k: usize,
966    },
967    SpoolLargeResult {
968        call_id: String,
969        tool: String,
970        output: String,
971        original_size: u32,
972        preview_size: u32,
973    },
974    ArchivePageOut {
975        turn: u32,
976        action: KernelPressureAction,
977        summary: Option<String>,
978        archived: Vec<Message>,
979        tier: String,
980    },
981    EvaluateMilestone {
982        phase_id: String,
983        criteria: Vec<String>,
984        #[serde(default, skip_serializing_if = "Option::is_none")]
985        verifier: Option<crate::types::milestone::MilestoneVerifier>,
986        #[serde(default, skip_serializing_if = "Vec::is_empty")]
987        required_evidence: Vec<String>,
988    },
989    Done {
990        result: LoopResult,
991    },
992}
993
994impl From<LoopAction> for KernelEffect {
995    fn from(action: LoopAction) -> Self {
996        match action {
997            LoopAction::AwaitingResume => {
998                panic!("AwaitingResume must not be converted to KernelEffect")
999            }
1000            LoopAction::CallLLM { context, tools } => Self::CallProvider { context, tools },
1001            LoopAction::ExecuteTools { calls } => Self::ExecuteTool { calls },
1002            LoopAction::RequestApproval { requests } => Self::RequestApproval { requests },
1003            LoopAction::SpawnWorkflow { nodes, budget } => Self::SpawnWorkflow { nodes, budget },
1004            LoopAction::PreemptSubAgents { agent_ids, reason } => {
1005                Self::PreemptSubAgents { agent_ids, reason }
1006            }
1007            LoopAction::PersistMemory { memory } => Self::PersistMemory { memory },
1008            LoopAction::QueryMemory { query, requested_k } => {
1009                Self::QueryMemory { query, requested_k }
1010            }
1011            LoopAction::SpoolLargeResult {
1012                call_id,
1013                tool,
1014                output,
1015                original_size,
1016                preview_size,
1017            } => Self::SpoolLargeResult {
1018                call_id,
1019                tool,
1020                output,
1021                original_size,
1022                preview_size,
1023            },
1024            LoopAction::ArchivePageOut {
1025                turn,
1026                action,
1027                summary,
1028                archived,
1029                tier,
1030            } => Self::ArchivePageOut {
1031                turn,
1032                action,
1033                summary,
1034                archived,
1035                tier,
1036            },
1037            LoopAction::EvaluateMilestone {
1038                phase_id,
1039                criteria,
1040                verifier,
1041                required_evidence,
1042            } => Self::EvaluateMilestone {
1043                phase_id,
1044                criteria,
1045                verifier,
1046                required_evidence,
1047            },
1048            LoopAction::Done { result } => Self::Done { result },
1049        }
1050    }
1051}
1052
1053#[derive(Debug, Clone, Serialize, Deserialize)]
1054#[serde(tag = "kind", rename_all = "snake_case")]
1055pub enum KernelObservation {
1056    /// Synchronous in-kernel compaction fact. Archived content is carried only by
1057    /// `ArchivePageOut`; it never rides an observation into host I/O.
1058    Compressed {
1059        #[serde(default)]
1060        turn: u32,
1061        action: KernelPressureAction,
1062        rho_after: f64,
1063        summary: Option<String>,
1064        archived_count: u32,
1065        /// W1-1 cache-awareness: the message index at which this compression invalidated the
1066        /// prompt cache prefix (if any). `None` = prefix-safe. SDK/telemetry can use this to
1067        /// quantify "tokens saved vs cache rebuild cost". Additive ABI field with default.
1068        #[serde(default, skip_serializing_if = "Option::is_none")]
1069        invalidates_prefix_at: Option<usize>,
1070    },
1071    Renewed {
1072        sprint: u32,
1073    },
1074    /// Rendering proved that fixed context or the protected transaction tail cannot fit inside the
1075    /// declared input budget. No provider effect is emitted for this turn.
1076    ContextBudgetExceeded {
1077        turn: u32,
1078        overflow_kind: crate::context::renderer::ContextBudgetOverflowKind,
1079        required_tokens: u32,
1080        max_tokens: u32,
1081    },
1082    /// K1: a boundary sweep of the knowledge partition applied deferred upserts and/or dropped
1083    /// marked entries. `removed_keys` lists keyed removals (unkeyed drops count only in
1084    /// `tokens_freed`); an upsert-only sweep has empty `removed_keys`.
1085    KnowledgeSwept {
1086        turn: u32,
1087        #[serde(default, skip_serializing_if = "Vec::is_empty")]
1088        removed_keys: Vec<String>,
1089        tokens_freed: u32,
1090    },
1091    /// K2: the knowledge partition exceeds its configured budget share. Fired at most once per
1092    /// cache generation; the over-budget unpinned entries are already marked for the next
1093    /// boundary sweep. Pinned/skill weight that cannot be evicted keeps the warning standing.
1094    KnowledgeBudgetExceeded {
1095        turn: u32,
1096        used: u32,
1097        budget: u32,
1098    },
1099    Rollbacked {
1100        turn: u32,
1101        checkpoint_history_len: u32,
1102        #[serde(default, skip_serializing_if = "Option::is_none")]
1103        reason: Option<RollbackReason>,
1104    },
1105    /// A control-plane request was rejected before its effect started. Unlike `Rollbacked`, this is
1106    /// a committed result: there is no transaction to undo, and hosts can route the reason back to
1107    /// the caller without mistaking a missing success observation for an internal failure.
1108    ControlRequestRejected {
1109        turn: u32,
1110        operation: String,
1111        #[serde(default, skip_serializing_if = "Option::is_none")]
1112        subject: Option<String>,
1113        reason: String,
1114    },
1115    CapabilityChanged {
1116        turn: u32,
1117        #[serde(default, skip_serializing_if = "Vec::is_empty")]
1118        added: Vec<String>,
1119        #[serde(default, skip_serializing_if = "Vec::is_empty")]
1120        removed: Vec<String>,
1121        #[serde(default, skip_serializing_if = "Option::is_none")]
1122        change_kind: Option<String>,
1123        #[serde(default, skip_serializing_if = "Option::is_none")]
1124        capability_id: Option<String>,
1125        #[serde(default, skip_serializing_if = "Option::is_none")]
1126        version: Option<String>,
1127        #[serde(default, skip_serializing_if = "Option::is_none")]
1128        mounted_by: Option<String>,
1129        #[serde(default, skip_serializing_if = "Option::is_none")]
1130        mount_reason: Option<String>,
1131    },
1132    MilestoneAdvanced {
1133        turn: u32,
1134        phase_id: String,
1135        capabilities_unlocked: Vec<String>,
1136    },
1137    MilestoneBlocked {
1138        turn: u32,
1139        phase_id: String,
1140        reason: String,
1141    },
1142    /// Checkpoint taken at the start of a turn transaction (before LLM call).
1143    CheckpointTaken {
1144        turn: u32,
1145        history_len: u32,
1146    },
1147    /// O6: the repeat fuse tripped — the same turn signature (non-meta tool name AND args) was
1148    /// re-issued `count`x consecutively. `action` = `"deny"` (turn rolled back, directive note fed
1149    /// back) or `"terminate"` (run ends `no_progress` after one final report turn). Additive ABI.
1150    RepeatFuseTripped {
1151        turn: u32,
1152        signature: String,
1153        count: u32,
1154        action: String,
1155    },
1156    /// O4: the turn-end criteria gate fired — the model tried to finish while acceptance criteria
1157    /// stand; the kernel injected one self-check turn before accepting `Completed`. Additive ABI.
1158    CriteriaGateFired {
1159        turn: u32,
1160        criteria: Vec<String>,
1161    },
1162    /// Session-entropy sample at a completed turn boundary (the heartbeat watch source).
1163    /// One per completed turn, unconditional — like `CheckpointTaken`. The component
1164    /// vector is the contract; `score` is a versioned default fold (`score_version`).
1165    /// See `scheduler::entropy`. Additive ABI.
1166    EntropySample {
1167        turn: u32,
1168        score: f64,
1169        score_version: u32,
1170        rho: f64,
1171        repeat_pressure: f64,
1172        failure_rate: f64,
1173        rollbacks_in_window: u32,
1174        window_turns: u32,
1175    },
1176    /// The opt-in entropy watch tripped: `score` crossed `threshold` while armed and
1177    /// cooled down (`EntropyWatchConfig`). Correlate components via the same-turn
1178    /// `EntropySample`. Additive ABI.
1179    EntropyAlert {
1180        turn: u32,
1181        score: f64,
1182        threshold: f64,
1183    },
1184    /// Kernel process table changed for a spawned sub-agent.
1185    AgentProcessChanged {
1186        turn: u32,
1187        agent_id: String,
1188        parent_session_id: String,
1189        role: String,
1190        isolation: String,
1191        context_inheritance: String,
1192        state: String,
1193        #[serde(default, skip_serializing_if = "Vec::is_empty")]
1194        permitted_capability_ids: Vec<String>,
1195        #[serde(default, skip_serializing_if = "Option::is_none")]
1196        result_termination: Option<String>,
1197    },
1198    /// W0-ABI: a workflow batch was spawned — each node's spawn descriptor (agent id + goal +
1199    /// role/isolation/inheritance) so the SDK can run the kernel-generated nodes.
1200    WorkflowBatchSpawned {
1201        turn: u32,
1202        nodes: Vec<crate::orchestration::workflow::WorkflowSpawnInfo>,
1203        /// G4 budget-as-signal: the workflow's remaining headroom under the active quota at spawn
1204        /// time, so a coordinator node can scale its next submission. Additive: omitted when no
1205        /// resource quota is installed (nothing to report).
1206        #[serde(default, skip_serializing_if = "Option::is_none")]
1207        budget: Option<crate::orchestration::workflow::WorkflowBudget>,
1208    },
1209    /// The host could not resolve a workflow spawn effect. No node is recorded
1210    /// as started; the same logical batch remains pending for retry.
1211    WorkflowSpawnFailed {
1212        turn: u32,
1213        error: String,
1214    },
1215    /// W0-ABI: a workflow finished (all nodes terminal, or stalled by a gated dependency).
1216    WorkflowCompleted {
1217        turn: u32,
1218        node_outcomes: Vec<crate::orchestration::workflow::run::WorkflowNodeOutcome>,
1219    },
1220    /// #2-B: a high-urgency `InterruptNow` signal preempted in-flight work. The kernel has already
1221    /// marked these agents `Done(UserAbort)` and reclaimed the root to reason about the interrupt; the
1222    /// SDK must ABORT the listed in-flight child runs and discard their results (do NOT feed their
1223    /// `SubAgentCompleted`). Additive variant (`agent_preempted`) — byte-identical for SDKs that never
1224    /// receive it.
1225    AgentPreempted {
1226        turn: u32,
1227        #[serde(default, skip_serializing_if = "Vec::is_empty")]
1228        agent_ids: Vec<String>,
1229        reason: String,
1230    },
1231    AgentPreemptFailed {
1232        turn: u32,
1233        agent_ids: Vec<String>,
1234        reason: String,
1235        error: String,
1236    },
1237    /// ③ loop-agent pacing: the kernel adjudicated a `pace` proposal for this round.
1238    RoundPaced {
1239        turn: u32,
1240        round: u32,
1241        decision: crate::types::result::PaceDecision,
1242    },
1243    /// R3-1: a runtime node submission was appended to the in-flight DAG at `base`
1244    /// (the graph length before the append). The SDK records `base` on the
1245    /// `workflow_nodes_submitted` session event so resume can re-apply the batch at
1246    /// the exact original indices (gap-filling any interleaved runtime children).
1247    WorkflowNodesSubmitted {
1248        turn: u32,
1249        base: u32,
1250        count: u32,
1251        /// W-N3: the submitting node's agent id (`None` = host/bootstrap). Persisted so resume can
1252        /// DROP batches whose submitter re-runs (it will re-submit) instead of duplicating them.
1253        #[serde(default, skip_serializing_if = "Option::is_none")]
1254        submitter: Option<String>,
1255    },
1256    /// A runtime node batch was rejected before any graph mutation.
1257    NodesRejected {
1258        turn: u32,
1259        node_index: u32,
1260        reason: String,
1261    },
1262    /// A tool call needs user approval (governance `AskUser`). Not blocked by the
1263    /// kernel — the SDK must obtain approval before executing the named call.
1264    ToolGated {
1265        turn: u32,
1266        call_id: String,
1267        tool: String,
1268        reason: String,
1269    },
1270    /// A leased inbound signal delivery was routed by the in-kernel attention policy.
1271    SignalDeliveryDisposed {
1272        turn: u32,
1273        operation_id: String,
1274        delivery_id: String,
1275        attempt: u32,
1276        signal_id: String,
1277        disposition: String,
1278        queue_depth: u32,
1279    },
1280    SignalDisplaced {
1281        turn: u32,
1282        admitted_signal_id: String,
1283        displaced_signal_id: String,
1284        queue_depth: u32,
1285    },
1286    SignalExpired {
1287        turn: u32,
1288        signal_id: String,
1289        queue_depth: u32,
1290    },
1291    SignalsPending {
1292        turn: u32,
1293        depth: u32,
1294    },
1295    /// A budget axis (turns / tokens / wall-time) was exhausted.
1296    BudgetExceeded {
1297        turn: u32,
1298        budget: String,
1299        operation_id: String,
1300        #[serde(default, skip_serializing_if = "Option::is_none")]
1301        reservation_id: Option<String>,
1302    },
1303    /// Terminal local usage for one reservation. Emitted exactly once per operation.
1304    BudgetUsageReported {
1305        operation_id: String,
1306        reservation_id: String,
1307        tokens: u64,
1308        subagents: u32,
1309        rounds: u32,
1310    },
1311    /// A host cancellation was committed. Emitted exactly once by the accepted cancellation step.
1312    OperationCancelled {
1313        turn: u32,
1314        operation_id: String,
1315        reason: CancellationReason,
1316        #[serde(default, skip_serializing_if = "Vec::is_empty")]
1317        pending_call_ids: Vec<String>,
1318    },
1319    /// Loop entered `Suspended` state (awaiting human approval or sub-agent).
1320    Suspended {
1321        turn: u32,
1322        reason: String,
1323        #[serde(default, skip_serializing_if = "Vec::is_empty")]
1324        pending_calls: Vec<String>,
1325    },
1326    /// Loop resumed from `Suspended` state.
1327    Resumed {
1328        turn: u32,
1329        #[serde(default, skip_serializing_if = "Vec::is_empty")]
1330        approved: Vec<String>,
1331        #[serde(default, skip_serializing_if = "Vec::is_empty")]
1332        denied: Vec<String>,
1333    },
1334    ApprovalResolutionFailed {
1335        turn: u32,
1336        error: String,
1337    },
1338    /// Memory entry written successfully (Phase 7).
1339    MemoryWritten {
1340        turn: u32,
1341        record_id: String,
1342        scope: crate::mm::memory::MemoryScope,
1343        memory_kind: crate::mm::memory::MemoryKind,
1344        name: String,
1345        size_bytes: u32,
1346    },
1347    /// Memory validation failed (Phase 7).
1348    MemoryValidationFailed {
1349        turn: u32,
1350        record_id: String,
1351        error: String,
1352    },
1353    MemoryWriteFailed {
1354        turn: u32,
1355        record_id: String,
1356        error: String,
1357    },
1358    /// Memory query request (Phase 7).
1359    MemoryQueried {
1360        turn: u32,
1361        scope: crate::mm::memory::MemoryScope,
1362        query: String,
1363        requested_k: usize,
1364        requires_async_response: bool,
1365    },
1366    MemoryQueryFailed {
1367        turn: u32,
1368        scope: crate::mm::memory::MemoryScope,
1369        query: String,
1370        error: String,
1371    },
1372    /// M3: recall lifecycle was journaled for one or more recalled records. Derived from the routed
1373    /// hits (each carries its current count); the host mirrors the incremented counts into its
1374    /// durable store so recall history survives across sessions.
1375    MemoryRecalled {
1376        turn: u32,
1377        scope: crate::mm::memory::MemoryScope,
1378        recalls: Vec<crate::mm::memory::MemoryRecallLifecycle>,
1379    },
1380    /// M4: a recalled record crossed the promotion threshold. Advisory only — the host/model decides
1381    /// whether to pin it or promote its content into knowledge.
1382    PromotionSuggested {
1383        turn: u32,
1384        record_id: String,
1385        recall_count: u64,
1386    },
1387    /// Large tool result spooled (Layer 1).
1388    LargeResultSpooled {
1389        turn: u32,
1390        call_id: String,
1391        tool: String,
1392        original_size: u32,
1393        preview_size: u32,
1394        spool_ref: Option<String>,
1395    },
1396    LargeResultSpoolFailed {
1397        turn: u32,
1398        call_id: String,
1399        tool: String,
1400        error: String,
1401    },
1402    PageOutArchived {
1403        turn: u32,
1404        action: KernelPressureAction,
1405        summary: Option<String>,
1406        tier: String,
1407        message_count: u32,
1408        #[serde(default, skip_serializing_if = "Option::is_none")]
1409        archive_ref: Option<String>,
1410    },
1411    PageOutArchiveFailed {
1412        turn: u32,
1413        action: KernelPressureAction,
1414        tier: String,
1415        message_count: u32,
1416        error: String,
1417    },
1418}
1419
1420#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1421#[serde(rename_all = "snake_case")]
1422pub enum KernelPressureAction {
1423    None,
1424    SnipCompact,
1425    MicroCompact,
1426    ContextCollapse,
1427    AutoCompact,
1428}
1429
1430impl From<PressureAction> for KernelPressureAction {
1431    fn from(action: PressureAction) -> Self {
1432        match action {
1433            PressureAction::None => Self::None,
1434            PressureAction::SnipCompact => Self::SnipCompact,
1435            PressureAction::MicroCompact => Self::MicroCompact,
1436            PressureAction::ContextCollapse => Self::ContextCollapse,
1437            PressureAction::AutoCompact => Self::AutoCompact,
1438        }
1439    }
1440}