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    /// K2: max share of `max_tokens` the knowledge partition may occupy (see
347    /// `ContextConfig::knowledge_budget_ratio`). Absent ⇒ kernel default (0.25); `0.0` disables.
348    #[serde(default, skip_serializing_if = "Option::is_none")]
349    pub knowledge_budget_ratio: Option<f64>,
350    /// Entropy watch: opt-in threshold alerting over the per-turn session-entropy score
351    /// (see `SetEntropyWatch`). Absent ⇒ kernel default (disabled; sampling itself is
352    /// unconditional and unaffected).
353    #[serde(default, skip_serializing_if = "Option::is_none")]
354    pub entropy_watch: Option<crate::scheduler::entropy::EntropyWatchConfig>,
355}
356
357#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
358pub struct BudgetGrant {
359    pub reservation_id: String,
360    #[serde(default, skip_serializing_if = "Option::is_none")]
361    pub tokens: Option<u64>,
362    #[serde(default, skip_serializing_if = "Option::is_none")]
363    pub subagents: Option<u32>,
364    #[serde(default, skip_serializing_if = "Option::is_none")]
365    pub rounds: Option<u32>,
366}
367
368#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
369#[serde(rename_all = "snake_case")]
370pub enum CancellationReason {
371    User,
372    Deadline,
373    LeaseLost,
374    HostShutdown,
375}
376
377/// Build a [`GovernancePipeline`](crate::governance::pipeline::GovernancePipeline) from the ABI policy
378/// fields. Shared by the `LoadGovernancePolicy` event and the `ConfigureRun` bundle so the two can never
379/// drift in how they interpret rules / vetoes / rate-limits / constraints.
380pub(crate) fn build_governance_pipeline(
381    default_action: Option<PolicyAction>,
382    rules: Vec<PolicyRule>,
383    vetoed_tools: Vec<String>,
384    rate_limits: Vec<RateLimitSpec>,
385    constraints: Vec<ConstraintSpec>,
386) -> crate::governance::pipeline::GovernancePipeline {
387    use crate::governance::constraint::{ConstraintRule, ParamConstraint};
388    use crate::governance::permission::PermissionRule;
389    use crate::governance::rate_limit::RateLimit;
390    let default = default_action.unwrap_or(PolicyAction::Allow).into();
391    let mut pipeline = crate::governance::pipeline::GovernancePipeline::new(default);
392    for rule in rules {
393        pipeline.permission.add_rule(PermissionRule {
394            tool_pattern: rule.tool_pattern.into(),
395            action: rule.action.into(),
396        });
397    }
398    for tool in vetoed_tools {
399        pipeline.veto.block_tool(tool);
400    }
401    for rl in rate_limits {
402        pipeline.rate_limiter.set_limit(
403            rl.tool,
404            RateLimit {
405                max_calls: rl.max_calls,
406                window_ms: rl.window_ms,
407            },
408        );
409    }
410    for c in constraints {
411        let (tool_name, param_path, rule) = match c {
412            ConstraintSpec::Required { tool, path } => (tool, path, ConstraintRule::Required),
413            ConstraintSpec::Enum { tool, path, values } => {
414                (tool, path, ConstraintRule::Enum(values))
415            }
416            ConstraintSpec::Range {
417                tool,
418                path,
419                min,
420                max,
421            } => (tool, path, ConstraintRule::Range { min, max }),
422        };
423        pipeline.constraints.add(ParamConstraint {
424            tool_name,
425            param_path,
426            rule,
427        });
428    }
429    pipeline
430}
431
432#[derive(Debug, Clone, Serialize, Deserialize)]
433#[serde(tag = "kind", rename_all = "snake_case")]
434pub enum KernelInputEvent {
435    SetTools {
436        tools: Vec<ToolSchema>,
437    },
438    SetAvailableSkills {
439        skills: Vec<SkillMetadata>,
440    },
441    /// P1-B tool gating: the model loaded a skill (`name`). The SDK emits this when it resolves a
442    /// `skill` tool call. The kernel records it in the active-skill set and resolves the skill's
443    /// `allowed_tools` from the catalog to narrow the toolset on subsequent turns.
444    SkillActivated {
445        name: String,
446        /// K3: auto-deactivate after this many turns (`None` = permanent, the default). On expiry
447        /// the toolset re-widens and the skill's knowledge pin is boundary-swept — same path as
448        /// an explicit `SkillDeactivated`.
449        #[serde(default, skip_serializing_if = "Option::is_none")]
450        lease_turns: Option<u32>,
451    },
452    /// K3: host-driven skill deactivation (there is deliberately NO model-facing unload — it
453    /// invites thrash). The toolset re-widens at the next provider call (an epoch event, same
454    /// cache cost class as activation); the `skill:<name>` knowledge pin drops at the next
455    /// boundary sweep. Errs-open: not-active is a no-op.
456    SkillDeactivated {
457        name: String,
458    },
459    /// P1-B/D: configure the stable-core tool ids (always exposed under skill gating). Set once by
460    /// the SDK; empty/absent ⇒ skills narrow to exactly their declared tools + meta-tools.
461    SetStableCoreTools {
462        tool_ids: Vec<String>,
463    },
464    SetMemoryEnabled {
465        enabled: bool,
466    },
467    SetKnowledgeEnabled {
468        enabled: bool,
469    },
470    SetPlanToolEnabled {
471        enabled: bool,
472    },
473    SetTokenizer {
474        name: String,
475    },
476    AddSystemMessage {
477        content: String,
478        tokens: u32,
479    },
480    AddKnowledgeMessage {
481        content: String,
482        tokens: u32,
483        /// K1: entry identity. `Some` ⇒ upsert semantics (same key replaces at the next
484        /// boundary); `None` ⇒ legacy unkeyed append. Additive — old logs replay unchanged.
485        #[serde(default, skip_serializing_if = "Option::is_none")]
486        key: Option<String>,
487        /// K1: host-pinned entries are exempt from the K2 budget sweep.
488        #[serde(default, skip_serializing_if = "std::ops::Not::not")]
489        pinned: bool,
490    },
491    /// K1: mark a keyed knowledge entry for removal at the next compaction/renewal boundary.
492    /// Errs-open: unknown key is a no-op.
493    RemoveKnowledge {
494        key: String,
495    },
496    AddHistoryMessage {
497        message: Message,
498        tokens: Option<u32>,
499    },
500    PreloadHistory {
501        messages: Vec<Message>,
502    },
503    MountCapability {
504        capability: CapabilityDescriptor,
505    },
506    UnmountCapability {
507        capability_kind: CapabilityKind,
508        id: String,
509    },
510    LoadMilestoneContract {
511        contract: MilestoneContract,
512    },
513    /// Install a governance policy. Once loaded, every model-proposed tool call
514    /// is evaluated in-kernel before execution. Omitting this event leaves the
515    /// gate disabled (pre-governance behavior).
516    LoadGovernancePolicy {
517        #[serde(default)]
518        default_action: Option<PolicyAction>,
519        #[serde(default, skip_serializing_if = "Vec::is_empty")]
520        rules: Vec<PolicyRule>,
521        #[serde(default, skip_serializing_if = "Vec::is_empty")]
522        vetoed_tools: Vec<String>,
523        // COMPAT(gov-abi-additive): rate_limits/constraints are additive fields with
524        // serde(default) so older SDKs that omit them still deserialize. Safe to keep.
525        #[serde(default, skip_serializing_if = "Vec::is_empty")]
526        rate_limits: Vec<RateLimitSpec>,
527        #[serde(default, skip_serializing_if = "Vec::is_empty")]
528        constraints: Vec<ConstraintSpec>,
529    },
530    /// Atomically replace the complete signal-routing policy.
531    SetSignalPolicy {
532        policy: SignalPolicyConfig,
533    },
534    ForceCompact,
535    UpdateTask {
536        update: TaskUpdate,
537    },
538    StartRun {
539        task: RuntimeTask,
540        #[serde(default, skip_serializing_if = "Option::is_none")]
541        run_spec: Option<AgentRunSpec>,
542    },
543    /// K2: apply a bundle of run-setup configuration in a single event. Every field is optional;
544    /// an absent field leaves that aspect untouched. This is a strict single-version contract:
545    /// unknown and superseded policy fields are rejected rather than silently ignored.
546    ConfigureRun {
547        config: RunConfig,
548    },
549    CapabilityCommand {
550        command: CapabilityCommand,
551    },
552    /// Continue a run reconstructed from preloaded history. Approval resolution
553    /// uses the correlated `ApprovalResult` event instead.
554    Resume,
555    ApprovalResult {
556        effect_id: String,
557        #[serde(default, skip_serializing_if = "Vec::is_empty")]
558        approved_calls: Vec<String>,
559        #[serde(default, skip_serializing_if = "Vec::is_empty")]
560        denied_calls: Vec<String>,
561        #[serde(default, skip_serializing_if = "Option::is_none")]
562        error: Option<String>,
563    },
564    /// Result of a host-owned workflow spawn batch. Every requested agent must
565    /// appear in exactly one of `started_agent_ids` or `failures`.
566    WorkflowSpawnResult {
567        effect_id: String,
568        #[serde(default, skip_serializing_if = "Vec::is_empty")]
569        started_agent_ids: Vec<String>,
570        #[serde(default, skip_serializing_if = "Vec::is_empty")]
571        failures: Vec<WorkflowSpawnFailure>,
572        #[serde(default, skip_serializing_if = "Option::is_none")]
573        error: Option<String>,
574    },
575    PreemptResult {
576        effect_id: String,
577        #[serde(default, skip_serializing_if = "Option::is_none")]
578        error: Option<String>,
579    },
580    MemoryPersistResult {
581        effect_id: String,
582        #[serde(default, skip_serializing_if = "Option::is_none")]
583        error: Option<String>,
584    },
585    MemoryQueryResult {
586        effect_id: String,
587        #[serde(default, skip_serializing_if = "Vec::is_empty")]
588        hits: Vec<crate::mm::memory::MemoryRecall>,
589        #[serde(default, skip_serializing_if = "Option::is_none")]
590        error: Option<String>,
591    },
592    LargeResultSpoolResult {
593        effect_id: String,
594        #[serde(default, skip_serializing_if = "Option::is_none")]
595        spool_ref: Option<String>,
596        #[serde(default, skip_serializing_if = "Option::is_none")]
597        error: Option<String>,
598    },
599    PageOutArchiveResult {
600        effect_id: String,
601        #[serde(default, skip_serializing_if = "Option::is_none")]
602        archive_ref: Option<String>,
603        #[serde(default, skip_serializing_if = "Option::is_none")]
604        error: Option<String>,
605    },
606    /// K2: set the knowledge-budget ratio at runtime (granular sibling of
607    /// `RunConfig::knowledge_budget_ratio`). `0.0` disables the cap.
608    SetKnowledgeBudget {
609        ratio: f64,
610    },
611    /// O4: enable/disable the turn-end criteria gate (default enabled; no-op for runs without
612    /// criteria). Additive ABI.
613    SetCriteriaGate {
614        enabled: bool,
615    },
616    /// O6: tune or disable the repeat fuse (defaults: enabled, deny_after=5, terminate_after=8).
617    /// Each field is optional — an absent field keeps the current value. Additive ABI.
618    SetRepeatFuse {
619        #[serde(default, skip_serializing_if = "Option::is_none")]
620        enabled: Option<bool>,
621        #[serde(default, skip_serializing_if = "Option::is_none")]
622        deny_after: Option<u32>,
623        #[serde(default, skip_serializing_if = "Option::is_none")]
624        terminate_after: Option<u32>,
625    },
626    /// Entropy watch: tune the opt-in threshold alerting over the per-turn session-entropy
627    /// score (defaults: disabled, threshold=0.65, hysteresis=0.1, cooldown_turns=4,
628    /// notify_model=false). Each field is optional — an absent field keeps the current
629    /// value. Sampling itself is unconditional and unaffected. Additive ABI.
630    SetEntropyWatch {
631        #[serde(default, skip_serializing_if = "Option::is_none")]
632        enabled: Option<bool>,
633        #[serde(default, skip_serializing_if = "Option::is_none")]
634        threshold: Option<f64>,
635        #[serde(default, skip_serializing_if = "Option::is_none")]
636        hysteresis: Option<f64>,
637        #[serde(default, skip_serializing_if = "Option::is_none")]
638        cooldown_turns: Option<u32>,
639        #[serde(default, skip_serializing_if = "Option::is_none")]
640        notify_model: Option<bool>,
641    },
642    /// Adjust the wall-clock budget at runtime (e.g. to extend or set a deadline
643    /// after a run has already started). Additive: omit to keep the value from
644    /// `SchedulerBudget` passed at construction.
645    SetSchedulerBudget {
646        #[serde(default, skip_serializing_if = "Option::is_none")]
647        max_wall_ms: Option<u64>,
648    },
649    /// M2 资源配额: install a declarative [`crate::governance::quota::ResourceQuota`] at the
650    /// single syscall trap. Like governance/attention/scheduler config, quotas flow in through
651    /// the versioned JSON event ABI (replayable, session-loggable) rather than a side-channel
652    /// setter — sending it is opt-in, and omitting it preserves the pre-M2 unconditional `Allow`
653    /// for spawn / memory-write syscalls.
654    SetResourceQuota {
655        quota: crate::governance::quota::ResourceQuota,
656    },
657    ProviderResult {
658        effect_id: String,
659        message: Message,
660        #[serde(default, skip_serializing_if = "Option::is_none")]
661        observed_input_tokens: Option<u32>,
662        #[serde(default, skip_serializing_if = "Option::is_none")]
663        observed_output_tokens: Option<u32>,
664        // COMPAT(gov-clock): now_ms is optional so SDKs that don't drive the in-kernel
665        // governance gate need not supply a clock. When absent, the rate limiter runs
666        // on a 0 clock (effectively unlimited). Can become required once all SDKs feed time.
667        #[serde(default, skip_serializing_if = "Option::is_none")]
668        now_ms: Option<u64>,
669        /// Provider stop_reason for this response — `max_tokens` (Anthropic) / `length` (OpenAI)
670        /// signal an output-cap truncation, which drives the kernel's max-output-tokens recovery.
671        /// Additive: omitted by providers/SDKs that don't report it (no-op recovery).
672        #[serde(default, skip_serializing_if = "Option::is_none")]
673        stop_reason: Option<String>,
674    },
675    ToolResults {
676        effect_id: String,
677        results: Vec<ToolResult>,
678    },
679    /// Reactive recovery entry point: the SDK's provider stream failed. The kernel classifies the
680    /// error (context-overflow vs other) and runs the bounded compact-and-retry recovery ladder,
681    /// returning `CallProvider` to retry with a freshly compacted context or `Done` to terminate.
682    /// The runners forward the raw provider error text and dispatch the result, instead of each
683    /// owning the classify + compact + retry + give-up policy. Additive ABI: a brand-new variant,
684    /// byte-identical on the wire for SDKs that never send it.
685    ProviderError {
686        effect_id: String,
687        message: String,
688    },
689    DeliverSignal {
690        delivery_id: String,
691        attempt: u32,
692        signal: RuntimeSignal,
693    },
694    MilestoneResult {
695        effect_id: String,
696        result: MilestoneCheckResult,
697    },
698    /// Spawn a sub-agent: registers/updates the kernel process table.
699    SpawnSubAgent {
700        spec: AgentRunSpec,
701        parent_session_id: String,
702    },
703    /// W0-ABI: load a workflow DAG and spawn its first gated batch. The kernel drives the DAG;
704    /// each node spawn passes the syscall trap and is reported via `workflow_batch_spawned`.
705    /// Completions feed back through `SubAgentCompleted` (reused); finish emits
706    /// `workflow_completed`.
707    LoadWorkflow {
708        spec: crate::orchestration::workflow::WorkflowSpec,
709        parent_session_id: String,
710        /// R3-1 resume: the runtime `submit_workflow_nodes` batches (in order) recovered from the log,
711        /// re-applied before completions so dynamically-appended nodes are reconstructed. Additive:
712        /// empty for a fresh run or a resume without dynamic submissions.
713        #[serde(default, skip_serializing_if = "Vec::is_empty")]
714        resumed_submissions: Vec<Vec<crate::orchestration::workflow::WorkflowNode>>,
715        /// Exact base graph index for every recovered submission batch. Length must equal
716        /// `resumed_submissions`; mismatch rejects the resume atomically.
717        #[serde(default)]
718        resumed_submission_bases: Vec<u32>,
719        /// Typed recovered terminal outcomes plus control signals. Status, termination and output
720        /// are mandatory facts for exact dependency-policy replay; bare completed ids are invalid.
721        #[serde(default, skip_serializing_if = "Vec::is_empty")]
722        resumed_outcomes: Vec<crate::orchestration::workflow::ResumedNodeOutcome>,
723    },
724    /// Feed a completed sub-agent result back into the parent loop.
725    SubAgentCompleted {
726        result: SubAgentResult,
727    },
728    /// R3-1: append nodes to the in-flight workflow DAG at runtime (dynamic fan-out /
729    /// loop-until-done). Sent by the SDK while the submitting node is still running — the appended
730    /// nodes spawn on the next gated drive. No-op if no workflow is active. Additive ABI: a brand-new
731    /// event variant, so existing SDKs that never send it are byte-identical on the wire.
732    SubmitWorkflowNodes {
733        #[serde(default, skip_serializing_if = "Vec::is_empty")]
734        nodes: Vec<crate::orchestration::workflow::WorkflowNode>,
735        /// G1: the agent id of the node that requested this submission. When it names a quarantined
736        /// node, the kernel coerces every submitted node to quarantined (no privilege escalation
737        /// across the trust boundary). Additive: omitted by older SDKs → `None` → no coercion.
738        #[serde(default, skip_serializing_if = "Option::is_none")]
739        submitter_agent_id: Option<String>,
740    },
741    /// M5/G1: an agent authors a whole `WorkflowSpec` (the article's "model writes its own harness").
742    /// The agent-reachable analogue of the host-only `LoadWorkflow`: **bootstraps** the DAG when no
743    /// workflow is active, else **flattens** the spec's nodes onto the running DAG (bootstrap-or-flatten,
744    /// one kernel / one quota — never a workflow stack). Gated by `Syscall::LoadWorkflow`. Additive ABI:
745    /// a brand-new variant, byte-identical on the wire for SDKs that never send it.
746    SubmitWorkflow {
747        spec: crate::orchestration::workflow::WorkflowSpec,
748        /// Used only on bootstrap (no workflow active) to seed child session ids; ignored on flatten.
749        #[serde(default)]
750        parent_session_id: String,
751        /// G1: the authoring node's agent id (flatten case) — a quarantined author's nodes are coerced
752        /// quarantined. Additive: omitted (top-level bootstrap) → `None` → the run's own trust applies.
753        #[serde(default, skip_serializing_if = "Option::is_none")]
754        submitter_agent_id: Option<String>,
755    },
756    /// Feed long-term memory entries into the knowledge partition (page-in).
757    /// SDK performs retrieval I/O; kernel only applies the result.
758    PageIn {
759        #[serde(default, skip_serializing_if = "Vec::is_empty")]
760        entries: Vec<crate::mm::PageInEntry>,
761    },
762    /// Configure long-term memory management policy (Phase 7). Opt-in: installing the policy makes
763    /// `validation_enabled`, `retrieval_top_k`, and the optional size/name overrides authoritative.
764    SetMemoryPolicy {
765        #[serde(default)]
766        memory_path: String,
767        #[serde(default = "default_stale_days")]
768        stale_warning_days: u32,
769        #[serde(default = "default_top_k")]
770        retrieval_top_k: usize,
771        #[serde(default = "default_validation_enabled")]
772        validation_enabled: bool,
773        /// Override the validation content-size limit (bytes). Omit to keep the kernel default.
774        #[serde(default, skip_serializing_if = "Option::is_none")]
775        max_content_bytes: Option<u32>,
776        /// Override the validation name-length limit. Omit to keep the kernel default.
777        #[serde(default, skip_serializing_if = "Option::is_none")]
778        max_name_length: Option<usize>,
779        /// M4: recall count at which a record becomes a promotion candidate. Omit to disable.
780        #[serde(default, skip_serializing_if = "Option::is_none")]
781        promotion_recall_threshold: Option<u64>,
782    },
783    /// Write a long-term memory entry (SDK background agent calls this).
784    WriteMemory {
785        memory: crate::mm::memory::MemoryRecord,
786    },
787    /// Query long-term memory for context (kernel calls this; SDK responds asynchronously).
788    QueryMemory {
789        query: crate::mm::memory::MemoryQuery,
790    },
791    /// Privileged host control: commit a host-driven run (for example a standalone workflow)
792    /// after its kernel-owned work has completed. This supersedes any pending provider effect and
793    /// produces the ordinary `done` effect and terminal usage report.
794    CompleteRun,
795    /// Host cancellation fact. The host has already stopped external I/O; the kernel commits the
796    /// deterministic terminal transition and clears every pending effect/wait state.
797    CancelOperation {
798        operation_id: String,
799        reason: CancellationReason,
800        #[serde(default, skip_serializing_if = "Vec::is_empty")]
801        pending_call_ids: Vec<String>,
802    },
803}
804
805fn default_stale_days() -> u32 {
806    2
807}
808fn default_top_k() -> usize {
809    5
810}
811fn default_validation_enabled() -> bool {
812    true
813}
814
815#[derive(Debug, Clone, Serialize, Deserialize)]
816pub struct KernelStep {
817    pub version: u32,
818    pub operation_id: String,
819    pub input_event_id: String,
820    pub step_seq: u64,
821    pub actions: Vec<KernelAction>,
822    pub observations: Vec<KernelObservation>,
823    #[serde(default, skip_serializing_if = "Vec::is_empty")]
824    pub faults: Vec<KernelFault>,
825}
826
827impl KernelStep {
828    pub(super) fn empty(
829        operation_id: String,
830        input_event_id: String,
831        step_seq: u64,
832        observations: Vec<KernelObservation>,
833    ) -> Self {
834        Self {
835            version: KERNEL_ABI_VERSION,
836            operation_id,
837            input_event_id,
838            step_seq,
839            actions: Vec::new(),
840            observations,
841            faults: Vec::new(),
842        }
843    }
844
845    pub(super) fn single(
846        operation_id: String,
847        input_event_id: String,
848        step_seq: u64,
849        action: LoopAction,
850        observations: Vec<KernelObservation>,
851    ) -> Self {
852        let effect_id = format!("{operation_id}:step:{step_seq}:effect:0");
853        Self {
854            version: KERNEL_ABI_VERSION,
855            operation_id,
856            input_event_id: input_event_id.clone(),
857            step_seq,
858            actions: vec![KernelAction::from_loop(effect_id, input_event_id, action)],
859            observations,
860            faults: Vec::new(),
861        }
862    }
863
864    pub(super) fn fault(
865        operation_id: String,
866        input_event_id: String,
867        step_seq: u64,
868        fault: KernelFault,
869    ) -> Self {
870        Self {
871            version: KERNEL_ABI_VERSION,
872            operation_id,
873            input_event_id,
874            step_seq,
875            actions: Vec::new(),
876            observations: Vec::new(),
877            faults: vec![fault],
878        }
879    }
880}
881
882#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
883#[serde(rename_all = "snake_case")]
884pub enum KernelFaultCode {
885    VersionMismatch,
886    OperationMismatch,
887    InvalidLifecycle,
888    InvalidConfig,
889    ResourceLimitExceeded,
890    DuplicateEventConflict,
891    UnexpectedEffectResult,
892    TransactionConflict,
893    SnapshotIncompatible,
894}
895
896#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
897pub struct KernelFault {
898    pub code: KernelFaultCode,
899    pub message: String,
900    #[serde(default, skip_serializing_if = "Option::is_none")]
901    pub operation_id: Option<String>,
902    #[serde(default, skip_serializing_if = "Option::is_none")]
903    pub event_id: Option<String>,
904    #[serde(default, skip_serializing_if = "Option::is_none")]
905    pub effect_id: Option<String>,
906}
907
908#[derive(Debug, Clone, Serialize, Deserialize)]
909pub struct KernelAction {
910    pub effect_id: String,
911    pub causation_id: String,
912    #[serde(flatten)]
913    pub effect: KernelEffect,
914}
915
916impl KernelAction {
917    fn from_loop(effect_id: String, causation_id: String, action: LoopAction) -> Self {
918        Self {
919            effect_id,
920            causation_id,
921            effect: action.into(),
922        }
923    }
924}
925
926#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
927pub struct WorkflowSpawnFailure {
928    pub agent_id: String,
929    pub error: String,
930}
931
932#[derive(Debug, Clone, Serialize, Deserialize)]
933#[serde(tag = "kind", rename_all = "snake_case")]
934pub enum KernelEffect {
935    CallProvider {
936        context: RenderedContext,
937        tools: Vec<ToolSchema>,
938    },
939    ExecuteTool {
940        calls: Vec<ToolCall>,
941    },
942    RequestApproval {
943        requests: Vec<crate::scheduler::state_machine::ApprovalRequest>,
944    },
945    SpawnWorkflow {
946        nodes: Vec<crate::orchestration::workflow::WorkflowSpawnInfo>,
947        #[serde(default, skip_serializing_if = "Option::is_none")]
948        budget: Option<crate::orchestration::workflow::WorkflowBudget>,
949    },
950    PreemptSubAgents {
951        agent_ids: Vec<String>,
952        reason: String,
953    },
954    PersistMemory {
955        memory: crate::mm::memory::MemoryRecord,
956    },
957    QueryMemory {
958        query: crate::mm::memory::MemoryQuery,
959        requested_k: usize,
960    },
961    SpoolLargeResult {
962        call_id: String,
963        tool: String,
964        output: String,
965        original_size: u32,
966        preview_size: u32,
967    },
968    ArchivePageOut {
969        turn: u32,
970        action: KernelPressureAction,
971        summary: Option<String>,
972        archived: Vec<Message>,
973        tier: String,
974    },
975    EvaluateMilestone {
976        phase_id: String,
977        criteria: Vec<String>,
978        #[serde(default, skip_serializing_if = "Option::is_none")]
979        verifier: Option<crate::types::milestone::MilestoneVerifier>,
980        #[serde(default, skip_serializing_if = "Vec::is_empty")]
981        required_evidence: Vec<String>,
982    },
983    Done {
984        result: LoopResult,
985    },
986}
987
988impl From<LoopAction> for KernelEffect {
989    fn from(action: LoopAction) -> Self {
990        match action {
991            LoopAction::AwaitingResume => {
992                panic!("AwaitingResume must not be converted to KernelEffect")
993            }
994            LoopAction::CallLLM { context, tools } => Self::CallProvider { context, tools },
995            LoopAction::ExecuteTools { calls } => Self::ExecuteTool { calls },
996            LoopAction::RequestApproval { requests } => Self::RequestApproval { requests },
997            LoopAction::SpawnWorkflow { nodes, budget } => Self::SpawnWorkflow { nodes, budget },
998            LoopAction::PreemptSubAgents { agent_ids, reason } => {
999                Self::PreemptSubAgents { agent_ids, reason }
1000            }
1001            LoopAction::PersistMemory { memory } => Self::PersistMemory { memory },
1002            LoopAction::QueryMemory { query, requested_k } => {
1003                Self::QueryMemory { query, requested_k }
1004            }
1005            LoopAction::SpoolLargeResult {
1006                call_id,
1007                tool,
1008                output,
1009                original_size,
1010                preview_size,
1011            } => Self::SpoolLargeResult {
1012                call_id,
1013                tool,
1014                output,
1015                original_size,
1016                preview_size,
1017            },
1018            LoopAction::ArchivePageOut {
1019                turn,
1020                action,
1021                summary,
1022                archived,
1023                tier,
1024            } => Self::ArchivePageOut {
1025                turn,
1026                action,
1027                summary,
1028                archived,
1029                tier,
1030            },
1031            LoopAction::EvaluateMilestone {
1032                phase_id,
1033                criteria,
1034                verifier,
1035                required_evidence,
1036            } => Self::EvaluateMilestone {
1037                phase_id,
1038                criteria,
1039                verifier,
1040                required_evidence,
1041            },
1042            LoopAction::Done { result } => Self::Done { result },
1043        }
1044    }
1045}
1046
1047#[derive(Debug, Clone, Serialize, Deserialize)]
1048#[serde(tag = "kind", rename_all = "snake_case")]
1049pub enum KernelObservation {
1050    /// Synchronous in-kernel compaction fact. Archived content is carried only by
1051    /// `ArchivePageOut`; it never rides an observation into host I/O.
1052    Compressed {
1053        #[serde(default)]
1054        turn: u32,
1055        action: KernelPressureAction,
1056        rho_after: f64,
1057        summary: Option<String>,
1058        archived_count: u32,
1059        /// W1-1 cache-awareness: the message index at which this compression invalidated the
1060        /// prompt cache prefix (if any). `None` = prefix-safe. SDK/telemetry can use this to
1061        /// quantify "tokens saved vs cache rebuild cost". Additive ABI field with default.
1062        #[serde(default, skip_serializing_if = "Option::is_none")]
1063        invalidates_prefix_at: Option<usize>,
1064    },
1065    Renewed {
1066        sprint: u32,
1067    },
1068    /// Rendering proved that fixed context or the protected transaction tail cannot fit inside the
1069    /// declared input budget. No provider effect is emitted for this turn.
1070    ContextBudgetExceeded {
1071        turn: u32,
1072        overflow_kind: crate::context::renderer::ContextBudgetOverflowKind,
1073        required_tokens: u32,
1074        max_tokens: u32,
1075    },
1076    /// K1: a boundary sweep of the knowledge partition applied deferred upserts and/or dropped
1077    /// marked entries. `removed_keys` lists keyed removals (unkeyed drops count only in
1078    /// `tokens_freed`); an upsert-only sweep has empty `removed_keys`.
1079    KnowledgeSwept {
1080        turn: u32,
1081        #[serde(default, skip_serializing_if = "Vec::is_empty")]
1082        removed_keys: Vec<String>,
1083        tokens_freed: u32,
1084    },
1085    /// K2: the knowledge partition exceeds its configured budget share. Fired at most once per
1086    /// cache generation; the over-budget unpinned entries are already marked for the next
1087    /// boundary sweep. Pinned/skill weight that cannot be evicted keeps the warning standing.
1088    KnowledgeBudgetExceeded {
1089        turn: u32,
1090        used: u32,
1091        budget: u32,
1092    },
1093    Rollbacked {
1094        turn: u32,
1095        checkpoint_history_len: u32,
1096        #[serde(default, skip_serializing_if = "Option::is_none")]
1097        reason: Option<RollbackReason>,
1098    },
1099    /// A control-plane request was rejected before its effect started. Unlike `Rollbacked`, this is
1100    /// a committed result: there is no transaction to undo, and hosts can route the reason back to
1101    /// the caller without mistaking a missing success observation for an internal failure.
1102    ControlRequestRejected {
1103        turn: u32,
1104        operation: String,
1105        #[serde(default, skip_serializing_if = "Option::is_none")]
1106        subject: Option<String>,
1107        reason: String,
1108    },
1109    CapabilityChanged {
1110        turn: u32,
1111        #[serde(default, skip_serializing_if = "Vec::is_empty")]
1112        added: Vec<String>,
1113        #[serde(default, skip_serializing_if = "Vec::is_empty")]
1114        removed: Vec<String>,
1115        #[serde(default, skip_serializing_if = "Option::is_none")]
1116        change_kind: Option<String>,
1117        #[serde(default, skip_serializing_if = "Option::is_none")]
1118        capability_id: Option<String>,
1119        #[serde(default, skip_serializing_if = "Option::is_none")]
1120        version: Option<String>,
1121        #[serde(default, skip_serializing_if = "Option::is_none")]
1122        mounted_by: Option<String>,
1123        #[serde(default, skip_serializing_if = "Option::is_none")]
1124        mount_reason: Option<String>,
1125    },
1126    MilestoneAdvanced {
1127        turn: u32,
1128        phase_id: String,
1129        capabilities_unlocked: Vec<String>,
1130    },
1131    MilestoneBlocked {
1132        turn: u32,
1133        phase_id: String,
1134        reason: String,
1135    },
1136    /// Checkpoint taken at the start of a turn transaction (before LLM call).
1137    CheckpointTaken {
1138        turn: u32,
1139        history_len: u32,
1140    },
1141    /// O6: the repeat fuse tripped — the same turn signature (non-meta tool name AND args) was
1142    /// re-issued `count`x consecutively. `action` = `"deny"` (turn rolled back, directive note fed
1143    /// back) or `"terminate"` (run ends `no_progress` after one final report turn). Additive ABI.
1144    RepeatFuseTripped {
1145        turn: u32,
1146        signature: String,
1147        count: u32,
1148        action: String,
1149    },
1150    /// O4: the turn-end criteria gate fired — the model tried to finish while acceptance criteria
1151    /// stand; the kernel injected one self-check turn before accepting `Completed`. Additive ABI.
1152    CriteriaGateFired {
1153        turn: u32,
1154        criteria: Vec<String>,
1155    },
1156    /// Session-entropy sample at a completed turn boundary (the heartbeat watch source).
1157    /// One per completed turn, unconditional — like `CheckpointTaken`. The component
1158    /// vector is the contract; `score` is a versioned default fold (`score_version`).
1159    /// See `scheduler::entropy`. Additive ABI.
1160    EntropySample {
1161        turn: u32,
1162        score: f64,
1163        score_version: u32,
1164        rho: f64,
1165        repeat_pressure: f64,
1166        failure_rate: f64,
1167        rollbacks_in_window: u32,
1168        window_turns: u32,
1169    },
1170    /// The opt-in entropy watch tripped: `score` crossed `threshold` while armed and
1171    /// cooled down (`EntropyWatchConfig`). Correlate components via the same-turn
1172    /// `EntropySample`. Additive ABI.
1173    EntropyAlert {
1174        turn: u32,
1175        score: f64,
1176        threshold: f64,
1177    },
1178    /// Kernel process table changed for a spawned sub-agent.
1179    AgentProcessChanged {
1180        turn: u32,
1181        agent_id: String,
1182        parent_session_id: String,
1183        role: String,
1184        isolation: String,
1185        context_inheritance: String,
1186        state: String,
1187        #[serde(default, skip_serializing_if = "Vec::is_empty")]
1188        permitted_capability_ids: Vec<String>,
1189        #[serde(default, skip_serializing_if = "Option::is_none")]
1190        result_termination: Option<String>,
1191    },
1192    /// W0-ABI: a workflow batch was spawned — each node's spawn descriptor (agent id + goal +
1193    /// role/isolation/inheritance) so the SDK can run the kernel-generated nodes.
1194    WorkflowBatchSpawned {
1195        turn: u32,
1196        nodes: Vec<crate::orchestration::workflow::WorkflowSpawnInfo>,
1197        /// G4 budget-as-signal: the workflow's remaining headroom under the active quota at spawn
1198        /// time, so a coordinator node can scale its next submission. Additive: omitted when no
1199        /// resource quota is installed (nothing to report).
1200        #[serde(default, skip_serializing_if = "Option::is_none")]
1201        budget: Option<crate::orchestration::workflow::WorkflowBudget>,
1202    },
1203    /// The host could not resolve a workflow spawn effect. No node is recorded
1204    /// as started; the same logical batch remains pending for retry.
1205    WorkflowSpawnFailed {
1206        turn: u32,
1207        error: String,
1208    },
1209    /// W0-ABI: a workflow finished (all nodes terminal, or stalled by a gated dependency).
1210    WorkflowCompleted {
1211        turn: u32,
1212        node_outcomes: Vec<crate::orchestration::workflow::run::WorkflowNodeOutcome>,
1213    },
1214    /// #2-B: a high-urgency `InterruptNow` signal preempted in-flight work. The kernel has already
1215    /// marked these agents `Done(UserAbort)` and reclaimed the root to reason about the interrupt; the
1216    /// SDK must ABORT the listed in-flight child runs and discard their results (do NOT feed their
1217    /// `SubAgentCompleted`). Additive variant (`agent_preempted`) — byte-identical for SDKs that never
1218    /// receive it.
1219    AgentPreempted {
1220        turn: u32,
1221        #[serde(default, skip_serializing_if = "Vec::is_empty")]
1222        agent_ids: Vec<String>,
1223        reason: String,
1224    },
1225    AgentPreemptFailed {
1226        turn: u32,
1227        agent_ids: Vec<String>,
1228        reason: String,
1229        error: String,
1230    },
1231    /// ③ loop-agent pacing: the kernel adjudicated a `pace` proposal for this round.
1232    RoundPaced {
1233        turn: u32,
1234        round: u32,
1235        decision: crate::types::result::PaceDecision,
1236    },
1237    /// R3-1: a runtime node submission was appended to the in-flight DAG at `base`
1238    /// (the graph length before the append). The SDK records `base` on the
1239    /// `workflow_nodes_submitted` session event so resume can re-apply the batch at
1240    /// the exact original indices (gap-filling any interleaved runtime children).
1241    WorkflowNodesSubmitted {
1242        turn: u32,
1243        base: u32,
1244        count: u32,
1245        /// W-N3: the submitting node's agent id (`None` = host/bootstrap). Persisted so resume can
1246        /// DROP batches whose submitter re-runs (it will re-submit) instead of duplicating them.
1247        #[serde(default, skip_serializing_if = "Option::is_none")]
1248        submitter: Option<String>,
1249    },
1250    /// A runtime node batch was rejected before any graph mutation.
1251    NodesRejected {
1252        turn: u32,
1253        node_index: u32,
1254        reason: String,
1255    },
1256    /// A tool call needs user approval (governance `AskUser`). Not blocked by the
1257    /// kernel — the SDK must obtain approval before executing the named call.
1258    ToolGated {
1259        turn: u32,
1260        call_id: String,
1261        tool: String,
1262        reason: String,
1263    },
1264    /// A leased inbound signal delivery was routed by the in-kernel attention policy.
1265    SignalDeliveryDisposed {
1266        turn: u32,
1267        operation_id: String,
1268        delivery_id: String,
1269        attempt: u32,
1270        signal_id: String,
1271        disposition: String,
1272        queue_depth: u32,
1273    },
1274    SignalDisplaced {
1275        turn: u32,
1276        admitted_signal_id: String,
1277        displaced_signal_id: String,
1278        queue_depth: u32,
1279    },
1280    SignalExpired {
1281        turn: u32,
1282        signal_id: String,
1283        queue_depth: u32,
1284    },
1285    SignalsPending {
1286        turn: u32,
1287        depth: u32,
1288    },
1289    /// A budget axis (turns / tokens / wall-time) was exhausted.
1290    BudgetExceeded {
1291        turn: u32,
1292        budget: String,
1293        operation_id: String,
1294        #[serde(default, skip_serializing_if = "Option::is_none")]
1295        reservation_id: Option<String>,
1296    },
1297    /// Terminal local usage for one reservation. Emitted exactly once per operation.
1298    BudgetUsageReported {
1299        operation_id: String,
1300        reservation_id: String,
1301        tokens: u64,
1302        subagents: u32,
1303        rounds: u32,
1304    },
1305    /// A host cancellation was committed. Emitted exactly once by the accepted cancellation step.
1306    OperationCancelled {
1307        turn: u32,
1308        operation_id: String,
1309        reason: CancellationReason,
1310        #[serde(default, skip_serializing_if = "Vec::is_empty")]
1311        pending_call_ids: Vec<String>,
1312    },
1313    /// Loop entered `Suspended` state (awaiting human approval or sub-agent).
1314    Suspended {
1315        turn: u32,
1316        reason: String,
1317        #[serde(default, skip_serializing_if = "Vec::is_empty")]
1318        pending_calls: Vec<String>,
1319    },
1320    /// Loop resumed from `Suspended` state.
1321    Resumed {
1322        turn: u32,
1323        #[serde(default, skip_serializing_if = "Vec::is_empty")]
1324        approved: Vec<String>,
1325        #[serde(default, skip_serializing_if = "Vec::is_empty")]
1326        denied: Vec<String>,
1327    },
1328    ApprovalResolutionFailed {
1329        turn: u32,
1330        error: String,
1331    },
1332    /// Memory entry written successfully (Phase 7).
1333    MemoryWritten {
1334        turn: u32,
1335        record_id: String,
1336        scope: crate::mm::memory::MemoryScope,
1337        memory_kind: crate::mm::memory::MemoryKind,
1338        name: String,
1339        size_bytes: u32,
1340    },
1341    /// Memory validation failed (Phase 7).
1342    MemoryValidationFailed {
1343        turn: u32,
1344        record_id: String,
1345        error: String,
1346    },
1347    MemoryWriteFailed {
1348        turn: u32,
1349        record_id: String,
1350        error: String,
1351    },
1352    /// Memory query request (Phase 7).
1353    MemoryQueried {
1354        turn: u32,
1355        scope: crate::mm::memory::MemoryScope,
1356        query: String,
1357        requested_k: usize,
1358        requires_async_response: bool,
1359    },
1360    MemoryQueryFailed {
1361        turn: u32,
1362        scope: crate::mm::memory::MemoryScope,
1363        query: String,
1364        error: String,
1365    },
1366    /// M3: recall lifecycle was journaled for one or more recalled records. Derived from the routed
1367    /// hits (each carries its current count); the host mirrors the incremented counts into its
1368    /// durable store so recall history survives across sessions.
1369    MemoryRecalled {
1370        turn: u32,
1371        scope: crate::mm::memory::MemoryScope,
1372        recalls: Vec<crate::mm::memory::MemoryRecallLifecycle>,
1373    },
1374    /// M4: a recalled record crossed the promotion threshold. Advisory only — the host/model decides
1375    /// whether to pin it or promote its content into knowledge.
1376    PromotionSuggested {
1377        turn: u32,
1378        record_id: String,
1379        recall_count: u64,
1380    },
1381    /// Large tool result spooled (Layer 1).
1382    LargeResultSpooled {
1383        turn: u32,
1384        call_id: String,
1385        tool: String,
1386        original_size: u32,
1387        preview_size: u32,
1388        spool_ref: Option<String>,
1389    },
1390    LargeResultSpoolFailed {
1391        turn: u32,
1392        call_id: String,
1393        tool: String,
1394        error: String,
1395    },
1396    PageOutArchived {
1397        turn: u32,
1398        action: KernelPressureAction,
1399        summary: Option<String>,
1400        tier: String,
1401        message_count: u32,
1402        #[serde(default, skip_serializing_if = "Option::is_none")]
1403        archive_ref: Option<String>,
1404    },
1405    PageOutArchiveFailed {
1406        turn: u32,
1407        action: KernelPressureAction,
1408        tier: String,
1409        message_count: u32,
1410        error: String,
1411    },
1412}
1413
1414#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1415#[serde(rename_all = "snake_case")]
1416pub enum KernelPressureAction {
1417    None,
1418    SnipCompact,
1419    MicroCompact,
1420    ContextCollapse,
1421    AutoCompact,
1422}
1423
1424impl From<PressureAction> for KernelPressureAction {
1425    fn from(action: PressureAction) -> Self {
1426        match action {
1427            PressureAction::None => Self::None,
1428            PressureAction::SnipCompact => Self::SnipCompact,
1429            PressureAction::MicroCompact => Self::MicroCompact,
1430            PressureAction::ContextCollapse => Self::ContextCollapse,
1431            PressureAction::AutoCompact => Self::AutoCompact,
1432        }
1433    }
1434}