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