Skip to main content

deepstrike_sdk/runtime/
runner.rs

1use std::sync::atomic::{AtomicBool, AtomicU8, Ordering};
2use std::sync::Arc;
3
4use crate::runtime::sandboxed_skill::scan_skill_dir;
5use crate::runtime::skill_watcher::SkillWatcher;
6use async_stream::try_stream;
7use deepstrike_core::governance::quota::ResourceQuota;
8use deepstrike_core::mm::memory::{
9    MemoryAuthor, MemoryKind, MemoryPolicy, MemoryProvenance, MemoryQuery, MemoryRecall,
10    MemoryRecord, MemoryScope, MemoryTrustLevel,
11};
12use deepstrike_core::runtime::kernel::{
13    CancellationReason, KernelAction, KernelEffect, KernelInput, KernelInputEvent,
14    KernelObservation, KernelPressureAction, KernelReliabilityConfig, KernelRuntime, KernelStep,
15    RunConfig,
16};
17use deepstrike_core::runtime::session::SessionEvent;
18use deepstrike_core::scheduler::policy::SchedulerBudget as KernelBudget;
19use deepstrike_core::scheduler::policy::SchedulerPolicyConfig;
20use deepstrike_core::types::message::{Message, ToolCall};
21use deepstrike_core::types::milestone::MilestoneCheckResult;
22use deepstrike_core::types::signal::{
23    RuntimeSignal as KernelSignal, SignalSource as KernelSignalSource,
24    SignalType as KernelSignalType, Urgency,
25};
26use deepstrike_core::types::task::RuntimeTask;
27use futures::StreamExt;
28
29use crate::governance::Governance;
30use crate::knowledge::KnowledgeSource;
31use crate::memory::DreamStore;
32use crate::providers::{LLMProvider, StreamEvent};
33use crate::run_event::RunEvent;
34use crate::runtime::archive::ArchiveStore;
35use crate::runtime::execution_plane::{
36    ExecutionPlane, LocalExecutionPlane, PermissionRequest, PermissionRequestHandler,
37    PermissionResponse, RunContext, ToolSuspendHandler,
38};
39use crate::runtime::os_profile::{
40    assert_native_profile, GovernancePolicy, OsProfile, SignalPolicy,
41};
42use crate::runtime::provider_replay::{peek_provider_replay, seed_provider_replay_from_events};
43use crate::runtime::replay::{
44    is_mid_run, repair_entries_with_cap, replay_messages_with_cap,
45    replay_messages_with_cap_and_loader,
46};
47use crate::runtime::session_log::{SessionEntry, SessionLog};
48use crate::{Error, Result};
49use crate::{SignalDeliveryReceipt, SignalSource};
50use deepstrike_core::context::task_state::TaskUpdate;
51use deepstrike_core::runtime::repair::repair_llm_completed;
52
53/// Controls what the runner does when the state machine returns
54/// `EvaluateMilestone` — i.e., the LLM finished a turn but a milestone phase
55/// has not yet been evaluated.
56#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
57pub enum MilestonePolicy {
58    /// Wait for a verifier callback or suspend if none is configured (default).
59    #[default]
60    RequireVerifier,
61    /// Terminate the run immediately with `status = "milestone_pending"`.
62    Terminate,
63    /// Unconditionally pass every milestone phase.  Useful in unit tests and
64    /// capability-unlock–only scenarios where the criteria check is a no-op.
65    AutoPass,
66}
67
68#[derive(Debug, Clone)]
69pub struct MilestoneEvaluationContext {
70    pub phase_id: String,
71    pub criteria: Vec<String>,
72    pub required_evidence: Vec<String>,
73}
74
75pub type MilestoneEvaluationHandler = std::sync::Arc<
76    dyn Fn(
77            MilestoneEvaluationContext,
78        ) -> futures::future::BoxFuture<'static, Result<MilestoneCheckResult>>
79        + Send
80        + Sync,
81>;
82
83/// P0-C tool-gating telemetry: per-LLM-turn metrics, delivered to [`RuntimeOptions::on_turn_metrics`].
84/// Pure observation — no behavior change. `tools_exposed` vs `tools_called` quantifies over-exposure;
85/// consecutive equal `active_skill` values measure skill dwell `D`; the cache split gives the
86/// prompt-cache hit baseline. Mirrors the node SDK `TurnMetrics`.
87#[derive(Debug, Clone)]
88pub struct TurnMetrics {
89    pub turn: u32,
90    pub tools_exposed: usize,
91    pub tools_called: usize,
92    pub active_skill: Option<String>,
93    pub input_tokens: u32,
94    pub cache_read_tokens: u32,
95    pub cache_creation_tokens: u32,
96    /// I1: pro-rata per-slot attribution of `cache_read_tokens` (Anthropic only). Mirrors Node.
97    pub cache_read_tokens_by_slot: Option<crate::providers::CacheReadBySlot>,
98}
99
100/// Sink for per-turn [`TurnMetrics`]. Synchronous, infallible — it must never affect the run.
101pub type OnTurnMetricsHandler = std::sync::Arc<dyn Fn(TurnMetrics) + Send + Sync>;
102
103/// Configuration for a `RuntimeRunner` (aligned with Node/Python `RuntimeOptions`).
104pub struct RuntimeOptions {
105    pub provider: Box<dyn LLMProvider>,
106    pub execution_plane: Option<Box<dyn ExecutionPlane>>,
107    pub session_log: Option<Arc<dyn SessionLog>>,
108    pub compression_store: Option<Arc<dyn ArchiveStore>>,
109    /// Directory used by the built-in large-result spool effect executor.
110    /// Defaults to `.spool` when omitted.
111    pub spool_dir: Option<std::path::PathBuf>,
112    /// Bounded ABI-v2 replay, retry, and large-result policy. Omitted fields retain kernel defaults.
113    pub kernel_reliability: Option<KernelReliabilityConfig>,
114    /// When set, `execute` reuses this session id.
115    pub session_id: Option<String>,
116    pub max_tokens: u32,
117    pub max_turns: Option<u32>,
118    pub timeout_ms: Option<u64>,
119    pub extensions: Option<serde_json::Value>,
120    pub agent_id: Option<String>,
121    /// Required by host-generated memory queries and semantic page-out writes.
122    pub memory_scope: Option<MemoryScope>,
123    /// I4: optional run-start memory pre-fetch hook. The runner calls this once per run, before
124    /// the first LLM turn, with the goal string; each returned query becomes a `dream_store.search`
125    /// and the resulting hits page into the knowledge partition before turn 1. Mirrors the Node
126    /// SDK `preQueryMemory`. Sync-only in Rust today — async hosts can pre-compute. Errs-open
127    /// when `dream_store` or `agent_id` is missing.
128    pub pre_query_memory: Option<std::sync::Arc<dyn Fn(&str) -> Vec<MemoryQuery> + Send + Sync>>,
129    pub system_prompt: Option<String>,
130    pub initial_memory: Vec<String>,
131    pub skill_dir: Option<std::path::PathBuf>,
132    pub dream_store: Option<Box<dyn DreamStore>>,
133    pub knowledge_source: Option<Box<dyn KnowledgeSource>>,
134    pub signal_source: Option<Box<dyn SignalSource>>,
135    pub governance: Option<Arc<tokio::sync::Mutex<Governance>>>,
136    pub os_profile: Option<OsProfile>,
137    pub governance_policy: Option<GovernancePolicy>,
138    pub signal_policy: Option<SignalPolicy>,
139    pub scheduler_policy: Option<SchedulerPolicyConfig>,
140    pub resource_quota: Option<ResourceQuota>,
141    /// Opt-in long-term memory policy (`set_memory_policy`), enforced at the kernel memory traps.
142    pub memory_policy: Option<MemoryPolicy>,
143    pub tokenizer: Option<String>,
144    pub enable_plan_tool: Option<bool>,
145    pub on_tool_suspend: Option<ToolSuspendHandler>,
146    pub on_permission_request: Option<PermissionRequestHandler>,
147    /// How to handle `EvaluateMilestone` actions. Default: `RequireVerifier`.
148    pub milestone_policy: MilestonePolicy,
149    pub milestone_contract: Option<deepstrike_core::types::milestone::MilestoneContract>,
150    pub run_spec: Option<deepstrike_core::types::agent::AgentRunSpec>,
151    /// P0-A tool gating: a static per-run tool profile — only these tool ids (plus the
152    /// skill/memory/knowledge/update_plan meta-tools) are exposed to the model each turn.
153    /// Lowers to the same `capability_filter` sub-agents use; byte-stable across the run, so it
154    /// never busts the prompt-cache prefix. Augments `run_spec`'s filter when both are set;
155    /// synthesizes a minimal top-level spec otherwise. `None`/empty ⇒ no gating (no config = old).
156    pub allowed_tool_ids: Option<Vec<String>>,
157    /// P0-C: optional per-turn metrics sink for tool-gating telemetry (see [`TurnMetrics`]). Pure
158    /// observation; invoked once per LLM turn. Panics are not caught — keep the sink trivial.
159    pub on_turn_metrics: Option<OnTurnMetricsHandler>,
160    /// P1-B/D stable-core: tool ids always exposed under skill gating. Empty ⇒ skills narrow to
161    /// exactly their declared tools + meta-tools. Opt-in: no skill declaring tools ⇒ never engages.
162    pub stable_core_tool_ids: Vec<String>,
163    pub on_milestone_evaluate: Option<MilestoneEvaluationHandler>,
164}
165
166/// P0-A: compute the effective top-level run spec from an optional explicit `run_spec` and an
167/// optional `allowed_tool_ids` static profile. The profile sets the capability filter's allowed
168/// ids — augmenting an explicit spec, or synthesizing a minimal `custom`-role spec when none is
169/// given. Returns `None` when neither is set ⇒ no gating (no config = old behavior).
170fn build_run_spec(
171    explicit: Option<deepstrike_core::types::agent::AgentRunSpec>,
172    allowed_tool_ids: Option<&[String]>,
173    agent_id: Option<&str>,
174    session_id: &str,
175    goal: &str,
176) -> Option<deepstrike_core::types::agent::AgentRunSpec> {
177    use deepstrike_core::types::agent::{AgentIdentity, AgentRole, AgentRunSpec};
178    let profile = allowed_tool_ids.filter(|ids| !ids.is_empty());
179    match (explicit, profile) {
180        (Some(mut spec), Some(ids)) => {
181            spec.capability_filter.allowed_ids = ids.iter().map(|s| s.as_str().into()).collect();
182            Some(spec)
183        }
184        (Some(spec), None) => Some(spec),
185        (None, Some(ids)) => {
186            let mut spec = AgentRunSpec::new(
187                AgentIdentity::new(agent_id.unwrap_or("root"), session_id),
188                AgentRole::Custom,
189                goal.to_string(),
190            );
191            spec.capability_filter.allowed_ids = ids.iter().map(|s| s.as_str().into()).collect();
192            Some(spec)
193        }
194        (None, None) => None,
195    }
196}
197
198/// Orchestrates the agentic turn loop via the runtime kernel + session event log.
199pub struct RuntimeRunner {
200    opts: RuntimeOptions,
201    plane: Box<dyn ExecutionPlane>,
202    interrupted: AtomicBool,
203    cancellation_reason: AtomicU8,
204    active_kernel: std::sync::Mutex<Option<std::sync::Arc<std::sync::Mutex<KernelRuntime>>>>,
205    local_page_out_cache: std::sync::Mutex<Vec<Message>>,
206}
207
208impl RuntimeRunner {
209    pub fn new(mut opts: RuntimeOptions) -> Self {
210        let plane = opts
211            .execution_plane
212            .take()
213            .unwrap_or_else(|| Box::new(LocalExecutionPlane::new()));
214        Self {
215            opts,
216            plane,
217            interrupted: AtomicBool::new(false),
218            cancellation_reason: AtomicU8::new(0),
219            active_kernel: std::sync::Mutex::new(None),
220            local_page_out_cache: std::sync::Mutex::new(Vec::new()),
221        }
222    }
223
224    pub fn interrupt(&self) {
225        self.interrupt_with_reason(CancellationReason::User);
226    }
227
228    pub fn interrupt_with_reason(&self, reason: CancellationReason) {
229        self.cancellation_reason
230            .store(cancellation_reason_code(reason), Ordering::Relaxed);
231        self.interrupted.store(true, Ordering::Relaxed);
232    }
233
234    pub fn execution_plane(&self) -> &dyn ExecutionPlane {
235        self.plane.as_ref()
236    }
237
238    pub async fn write_memory(
239        &self,
240        memory: MemoryRecord,
241        session_id: Option<&str>,
242        agent_id: Option<&str>,
243    ) -> Result<()> {
244        let Some(store) = &self.opts.dream_store else {
245            return Ok(());
246        };
247        let Some(agent_id) = agent_id.or(self.opts.agent_id.as_deref()) else {
248            return Ok(());
249        };
250
251        let (kernel, requested) = self.begin_memory_syscall(KernelInputEvent::WriteMemory {
252            memory: memory.clone(),
253        });
254        let persist_effect = requested
255            .actions
256            .iter()
257            .find_map(|action| match &action.effect {
258                KernelEffect::PersistMemory { memory } => {
259                    Some((action.effect_id.clone(), memory.clone()))
260                }
261                _ => None,
262            });
263        let Some((effect_id, canonical)) = persist_effect else {
264            self.append_memory_syscall_observations(session_id, requested.observations)
265                .await;
266            return Ok(());
267        };
268
269        let io_result: Result<()> = async {
270            store.upsert(agent_id, canonical).await?;
271            Ok(())
272        }
273        .await;
274        let completed =
275            kernel
276                .lock()
277                .unwrap()
278                .step(KernelInput::new(KernelInputEvent::MemoryPersistResult {
279                    effect_id,
280                    error: io_result.as_ref().err().map(ToString::to_string),
281                }));
282        self.append_memory_syscall_observations(session_id, completed.observations)
283            .await;
284        io_result
285    }
286
287    pub async fn query_memory(
288        &self,
289        query: MemoryQuery,
290        session_id: Option<&str>,
291        agent_id: Option<&str>,
292    ) -> Result<Vec<MemoryRecall>> {
293        let Some(store) = &self.opts.dream_store else {
294            return Ok(Vec::new());
295        };
296        let Some(agent_id) = agent_id.or(self.opts.agent_id.as_deref()) else {
297            return Ok(Vec::new());
298        };
299
300        let (kernel, requested) = self.begin_memory_syscall(KernelInputEvent::QueryMemory {
301            query: query.clone(),
302        });
303        let query_effect = requested
304            .actions
305            .iter()
306            .find_map(|action| match &action.effect {
307                KernelEffect::QueryMemory { query, requested_k } => {
308                    Some((action.effect_id.clone(), query.clone(), *requested_k))
309                }
310                _ => None,
311            });
312        let Some((effect_id, mut canonical_query, requested_k)) = query_effect else {
313            self.append_memory_syscall_observations(session_id, requested.observations)
314                .await;
315            return Ok(Vec::new());
316        };
317
318        canonical_query.top_k = requested_k;
319        let hits = store.search(agent_id, &canonical_query).await?;
320        let completed =
321            kernel
322                .lock()
323                .unwrap()
324                .step(KernelInput::new(KernelInputEvent::MemoryQueryResult {
325                    effect_id,
326                    hits: hits.clone(),
327                    error: None,
328                }));
329        self.append_memory_syscall_observations(session_id, completed.observations)
330            .await;
331        self.log_memory_retrieval_result(session_id, hits.clone())
332            .await;
333        Ok(hits)
334    }
335
336    async fn extract_session_memories(
337        &self,
338        session: &deepstrike_core::memory::durable::SessionData,
339        scope: &MemoryScope,
340    ) -> Result<Vec<MemoryRecord>> {
341        let transcript = session
342            .messages
343            .iter()
344            .map(|message| {
345                format!(
346                    "[{:?}] {}",
347                    message.role,
348                    message.content.as_text().unwrap_or_default()
349                )
350            })
351            .collect::<Vec<_>>()
352            .join("\n")
353            .chars()
354            .take(8_000)
355            .collect::<String>();
356        let prompt = format!(
357            "{transcript}\n\nReturn {{\"memories\":[{{\"name\":\"stable-kebab-key\",\"kind\":\"user|feedback|project|reference\",\"content\":\"fact\",\"description\":\"why durable\",\"confidence\":0.0,\"links\":[],\"pinned\":false,\"ttl_days\":null,\"evidence_refs\":[]}}]}} with at most 10 items. Return {{\"memories\":[]}} when nothing is durable."
358        );
359        let context = rendered_context_from_messages(vec![
360            Message::system("Extract durable, reusable facts from this completed session. Return only JSON; do not include transient progress or guesses."),
361            Message::user(prompt),
362        ]);
363        let state = self.opts.provider.create_run_state();
364        let mut stream = self
365            .opts
366            .provider
367            .stream(&context, &[], None, state.as_ref())
368            .await?;
369        let mut output = String::new();
370        while let Some(event) = stream.next().await {
371            if let StreamEvent::TextDelta { delta } = event? {
372                output.push_str(&delta);
373            }
374        }
375        Ok(crate::memory::parse_extracted_memories(
376            &output, session, scope,
377        ))
378    }
379
380    async fn log_memory_retrieval_result(
381        &self,
382        session_id: Option<&str>,
383        hits: Vec<MemoryRecall>,
384    ) {
385        let Some(session_id) = session_id.or(self.opts.session_id.as_deref()) else {
386            return;
387        };
388        // The session-log record is the durable audit artifact; the kernel needs no
389        // acknowledgment (the former kernel event was a no-op and was removed).
390        self.log(
391            session_id,
392            SessionEvent::MemoryRetrievalResult { hits },
393        )
394        .await;
395    }
396
397    fn begin_memory_syscall(
398        &self,
399        event: KernelInputEvent,
400    ) -> (Arc<std::sync::Mutex<KernelRuntime>>, KernelStep) {
401        if let Some(active) = self.active_kernel.lock().unwrap().clone() {
402            if !active.lock().unwrap().is_terminal() {
403                let step = {
404                    let mut kernel = active.lock().unwrap();
405                    kernel.step(KernelInput::new(event))
406                };
407                return (active, step);
408            }
409        }
410
411        let mut kernel = KernelRuntime::new(KernelBudget {
412            max_tokens: self.opts.max_tokens,
413            max_turns: self.opts.max_turns.unwrap_or(25),
414            max_wall_ms: self.opts.timeout_ms,
415            ..Default::default()
416        });
417        if let Ok(profile) = assert_native_profile(self.opts.os_profile.clone()) {
418            kernel.step(KernelInput::new(
419                self.opts
420                    .governance_policy
421                    .clone()
422                    .unwrap_or(profile.governance_policy)
423                    .into_kernel_event(),
424            ));
425            let signal_policy = self.opts.signal_policy.unwrap_or(profile.signal_policy);
426            kernel.step(KernelInput::new(KernelInputEvent::SetSignalPolicy {
427                policy: signal_policy.into_kernel(),
428            }));
429        }
430        if let Some(scheduler_policy) = self.opts.scheduler_policy {
431            kernel.step(KernelInput::new(KernelInputEvent::ConfigureRun {
432                config: RunConfig {
433                    scheduler_policy: Some(scheduler_policy),
434                    ..RunConfig::default()
435                },
436            }));
437        }
438        if let Some(quota) = self.opts.resource_quota.clone() {
439            kernel.step(KernelInput::new(KernelInputEvent::SetResourceQuota {
440                quota,
441            }));
442        }
443        if let Some(policy) = self.opts.memory_policy.clone() {
444            kernel.step(KernelInput::new(memory_policy_event(policy)));
445        }
446        let kernel = Arc::new(std::sync::Mutex::new(kernel));
447        let step = kernel.lock().unwrap().step(KernelInput::new(event));
448        (kernel, step)
449    }
450
451    async fn append_memory_syscall_observations(
452        &self,
453        session_id: Option<&str>,
454        observations: Vec<KernelObservation>,
455    ) {
456        let Some(session_id) = session_id.or(self.opts.session_id.as_deref()) else {
457            return;
458        };
459        for obs in observations {
460            match obs {
461                KernelObservation::MemoryWritten {
462                    turn,
463                    record_id,
464                    scope,
465                    memory_kind,
466                    name,
467                    size_bytes,
468                } => {
469                    self.log(
470                        session_id,
471                        SessionEvent::MemoryWritten {
472                            turn,
473                            record_id,
474                            scope,
475                            memory_kind,
476                            name,
477                            size_bytes,
478                        },
479                    )
480                    .await;
481                }
482                KernelObservation::MemoryQueried {
483                    turn,
484                    scope,
485                    query,
486                    requested_k,
487                    requires_async_response,
488                } => {
489                    self.log(
490                        session_id,
491                        SessionEvent::MemoryQueried {
492                            turn,
493                            scope,
494                            query,
495                            requested_k,
496                            requires_async_response,
497                        },
498                    )
499                    .await;
500                }
501                KernelObservation::MemoryValidationFailed {
502                    turn,
503                    record_id,
504                    error,
505                } => {
506                    self.log(
507                        session_id,
508                        SessionEvent::MemoryValidationFailed {
509                            turn,
510                            record_id,
511                            error,
512                        },
513                    )
514                    .await;
515                }
516                _ => {}
517            }
518        }
519    }
520
521    pub async fn execute(&self, goal: &str) -> Result<String> {
522        collect_text(self.run_streaming(goal, &[], None, None).await?).await
523    }
524
525    pub async fn execute_with_criteria(&self, goal: &str, criteria: &[String]) -> Result<String> {
526        collect_text(self.run_streaming(goal, criteria, None, None).await?).await
527    }
528
529    pub async fn run_streaming<'a>(
530        &'a self,
531        goal: &'a str,
532        criteria: &'a [String],
533        extensions: Option<&'a serde_json::Value>,
534        session_id: Option<&'a str>,
535    ) -> Result<std::pin::Pin<Box<dyn futures::Stream<Item = Result<RunEvent>> + 'a>>> {
536        self.run_streaming_with_attachments(goal, criteria, extensions, session_id, &[])
537            .await
538    }
539
540    /// Like [`Self::run_streaming`], but seeds multimodal `attachments` into kernel history
541    /// before the first render (parity with Node/Python `run({ attachments })`).
542    pub async fn run_streaming_with_attachments<'a>(
543        &'a self,
544        goal: &'a str,
545        criteria: &'a [String],
546        extensions: Option<&'a serde_json::Value>,
547        session_id: Option<&'a str>,
548        attachments: &'a [deepstrike_core::types::message::ContentPart],
549    ) -> Result<std::pin::Pin<Box<dyn futures::Stream<Item = Result<RunEvent>> + 'a>>> {
550        let session_id = session_id
551            .map(str::to_string)
552            .or_else(|| self.opts.session_id.clone())
553            .unwrap_or_else(|| uuid::Uuid::new_v4().to_string());
554
555        let prior = self.read_entries(&session_id).await?;
556        let mid_run = is_mid_run(&prior);
557
558        if !mid_run {
559            self.log(
560                &session_id,
561                SessionEvent::RunStarted {
562                    run_id: uuid::Uuid::new_v4().to_string(),
563                    goal: goal.to_string(),
564                    criteria: criteria.to_vec(),
565                    agent_id: self.opts.agent_id.clone(),
566                    system_prompt: self.opts.system_prompt.clone(),
567                    attachments: attachments.to_vec(),
568                },
569            )
570            .await;
571        }
572
573        let goal_owned = goal.to_string();
574        let criteria_owned = criteria.to_vec();
575        let extensions_owned = extensions.cloned();
576        let attachments_owned = attachments.to_vec();
577        let prior_events = if prior.is_empty() { None } else { Some(prior) };
578
579        Ok(Box::pin(self.execute_inner(
580            session_id,
581            goal_owned,
582            criteria_owned,
583            extensions_owned,
584            prior_events,
585            mid_run,
586            attachments_owned,
587        )))
588    }
589
590    pub async fn wake_streaming(
591        &self,
592        session_id: &str,
593        extensions: Option<&serde_json::Value>,
594    ) -> Result<std::pin::Pin<Box<dyn futures::Stream<Item = Result<RunEvent>> + '_>>> {
595        let prior = self.read_entries(session_id).await?;
596        if prior
597            .iter()
598            .any(|e| matches!(e.event, SessionEvent::RunTerminal { .. }))
599        {
600            return Ok(Box::pin(futures::stream::empty()));
601        }
602        let start = prior
603            .iter()
604            .rev()
605            .find(|e| matches!(e.event, SessionEvent::RunStarted { .. }))
606            .ok_or_else(|| Error::Other(format!("no run_started for session: {session_id}")))?;
607
608        let (goal, criteria, attachments) = match &start.event {
609            SessionEvent::RunStarted {
610                goal,
611                criteria,
612                attachments,
613                ..
614            } => (goal.clone(), criteria.clone(), attachments.clone()),
615            _ => unreachable!(),
616        };
617
618        Ok(Box::pin(self.execute_inner(
619            session_id.to_string(),
620            goal,
621            criteria,
622            extensions.cloned(),
623            Some(prior),
624            true,
625            attachments,
626        )))
627    }
628
629    pub async fn wake(&self, session_id: &str) -> Result<String> {
630        collect_text(self.wake_streaming(session_id, None).await?).await
631    }
632
633    fn execute_inner(
634        &self,
635        session_id: String,
636        goal: String,
637        criteria: Vec<String>,
638        extensions: Option<serde_json::Value>,
639        prior_events: Option<Vec<SessionEntry>>,
640        resume_mid_run: bool,
641        attachments: Vec<deepstrike_core::types::message::ContentPart>,
642    ) -> impl futures::Stream<Item = Result<RunEvent>> + '_ {
643        try_stream! {
644            self.interrupted.store(false, Ordering::Relaxed);
645            self.cancellation_reason.store(0, Ordering::Relaxed);
646
647            if let Some(ks) = &self.opts.knowledge_source {
648                ks.init().await?;
649            }
650
651            let provider_policy = self.opts.provider.runtime_policy();
652            let effective_max_turns = self.opts.max_turns.or(provider_policy.max_turns).unwrap_or(25);
653            let effective_timeout = self.opts.timeout_ms.or(provider_policy.timeout_ms);
654            let policy = KernelBudget {
655                max_tokens: self.opts.max_tokens,
656                max_turns: effective_max_turns,
657                max_wall_ms: effective_timeout,
658                ..Default::default()
659            };
660
661            let mut kernel = std::sync::Arc::new(std::sync::Mutex::new(KernelRuntime::new(policy)));
662            {
663                let mut active = self.active_kernel.lock().unwrap();
664                *active = Some(kernel.clone());
665            }
666
667            struct ActiveKernelGuard<'a> {
668                runner: &'a RuntimeRunner,
669            }
670            impl<'a> Drop for ActiveKernelGuard<'a> {
671                fn drop(&mut self) {
672                    if let Ok(mut active) = self.runner.active_kernel.lock() {
673                        *active = None;
674                    }
675                }
676            }
677            let _guard = ActiveKernelGuard { runner: self };
678
679            let kernel_apply = |kernel_arc: &mut std::sync::Arc<std::sync::Mutex<KernelRuntime>>, pending: &mut Vec<KernelObservation>, event| {
680                kernel_apply(&mut *kernel_arc.lock().unwrap(), pending, event)
681            };
682            let kernel_action = |kernel_arc: &mut std::sync::Arc<std::sync::Mutex<KernelRuntime>>, pending: &mut Vec<KernelObservation>, event| {
683                kernel_action(&mut *kernel_arc.lock().unwrap(), pending, event)
684            };
685
686            let mut pending_observations = Vec::new();
687            let mut pending_page_out_starts = std::collections::VecDeque::new();
688            let mut active_page_out_start = None;
689
690            if self.opts.kernel_reliability.is_some() || self.opts.scheduler_policy.is_some() {
691                let mut step = kernel.lock().unwrap().step(KernelInput::new(
692                    KernelInputEvent::ConfigureRun {
693                        config: RunConfig {
694                            reliability: self.opts.kernel_reliability.clone(),
695                            scheduler_policy: self.opts.scheduler_policy,
696                            ..RunConfig::default()
697                        },
698                    },
699                ));
700                if let Some(fault) = step.faults.pop() {
701                    Err(Error::Other(format!(
702                        "kernel {:?}: {}",
703                        fault.code, fault.message
704                    )))?;
705                }
706                pending_observations.append(&mut step.observations);
707            }
708
709            if let Some(tokenizer_name) = &self.opts.tokenizer {
710                kernel_apply(
711                    &mut kernel,
712                    &mut pending_observations,
713                    KernelInputEvent::SetTokenizer {
714                        name: tokenizer_name.clone(),
715                    },
716                );
717            }
718            if let Some(enabled) = self.opts.enable_plan_tool {
719                kernel_apply(
720                    &mut kernel,
721                    &mut pending_observations,
722                    KernelInputEvent::SetPlanToolEnabled { enabled },
723                );
724            }
725
726            kernel_apply(
727                &mut kernel,
728                &mut pending_observations,
729                KernelInputEvent::SetTools {
730                    tools: self.plane.schemas(),
731                },
732            );
733
734            if self.opts.dream_store.is_some() && self.opts.agent_id.is_some() {
735                kernel_apply(
736                    &mut kernel,
737                    &mut pending_observations,
738                    KernelInputEvent::SetMemoryEnabled { enabled: true },
739                );
740            }
741            if self.opts.knowledge_source.is_some() {
742                kernel_apply(
743                    &mut kernel,
744                    &mut pending_observations,
745                    KernelInputEvent::SetKnowledgeEnabled { enabled: true },
746                );
747            }
748
749            if let Some(sp) = &self.opts.system_prompt {
750                let tokens = ((sp.len() / 4) as u32).max(1);
751                kernel_apply(
752                    &mut kernel,
753                    &mut pending_observations,
754                    KernelInputEvent::AddSystemMessage {
755                        content: sp.clone(),
756                        tokens,
757                    },
758                );
759            }
760            for mem in &self.opts.initial_memory {
761                let tokens = ((mem.len() / 4) as u32).max(1);
762                kernel_apply(
763                    &mut kernel,
764                    &mut pending_observations,
765                    KernelInputEvent::AddKnowledgeMessage {
766                        content: mem.clone(),
767                        tokens,
768                        key: None,
769                        pinned: false,
770                    },
771                );
772            }
773
774            let skill_watcher = self.opts.skill_dir.as_deref().and_then(SkillWatcher::start);
775            if let Some(skill_dir) = &self.opts.skill_dir {
776                kernel_apply(
777                    &mut kernel,
778                    &mut pending_observations,
779                    KernelInputEvent::SetAvailableSkills {
780                        skills: scan_skill_dir(skill_dir),
781                    },
782                );
783            }
784
785            // P1-B/D: configure stable-core tool ids (always exposed under skill gating).
786            if !self.opts.stable_core_tool_ids.is_empty() {
787                kernel_apply(
788                    &mut kernel,
789                    &mut pending_observations,
790                    KernelInputEvent::SetStableCoreTools {
791                        tool_ids: self.opts.stable_core_tool_ids.clone(),
792                    },
793                );
794            }
795
796            if let Some(milestones) = self.opts.milestone_contract.clone() {
797                kernel_apply(
798                    &mut kernel,
799                    &mut pending_observations,
800                    KernelInputEvent::LoadMilestoneContract { contract: milestones },
801                );
802            }
803
804            let max_bytes = {
805                let k = kernel.lock().unwrap();
806                k.recovery_content_bytes()
807            };
808
809            if let Some(ref events) = prior_events {
810                let repaired = repair_entries_with_cap(events, max_bytes);
811                seed_provider_replay_from_events(self.opts.provider.as_ref(), &repaired);
812
813                let messages = if let Some(ref store) = self.opts.compression_store {
814                    let store_clone = store.clone();
815                    replay_messages_with_cap_and_loader(&repaired, max_bytes, move |archive_ref| {
816                        store_clone.read(archive_ref).map_err(|_| {
817                            deepstrike_core::context::fault::ContextFault::MissingArchive {
818                                session_id: String::new(),
819                                seq: 0,
820                            }
821                        })
822                    })
823                } else {
824                    replay_messages_with_cap(&repaired, max_bytes)
825                };
826
827                // P1-B B3: collect skill activations from the replayed history before `messages` is
828                // moved, then re-emit them after preload to rebuild gating (active_skills is not
829                // snapshotted — graceful).
830                let reactivate: Vec<String> = messages
831                    .iter()
832                    .flat_map(|m| m.tool_calls.iter())
833                    .filter(|c| c.name.as_str() == "skill")
834                    .filter_map(|c| c.arguments.get("name").and_then(|v| v.as_str()).map(str::to_string))
835                    .collect();
836
837                kernel_apply(
838                    &mut kernel,
839                    &mut pending_observations,
840                    KernelInputEvent::PreloadHistory {
841                        messages,
842                    },
843                );
844
845                for name in reactivate {
846                    kernel_apply(
847                        &mut kernel,
848                        &mut pending_observations,
849                        KernelInputEvent::SkillActivated { name, lease_turns: None },
850                    );
851                }
852            }
853
854            let ext = merge_extensions(self.opts.extensions.as_ref(), extensions.as_ref());
855            let provider_state = self.opts.provider.create_run_state();
856            let mut next_archive_start = next_archived_seq_start(prior_events.as_deref());
857            // P0-C: the skill loaded and in effect going into the current turn → per-turn metric.
858            let mut active_skill: Option<String> = None;
859            let session_start_ms = std::time::SystemTime::now()
860                .duration_since(std::time::UNIX_EPOCH)
861                .unwrap_or_default()
862                .as_millis() as u64;
863
864            let os_profile = assert_native_profile(self.opts.os_profile.clone())?;
865            let governance_policy = self
866                .opts
867                .governance_policy
868                .clone()
869                .unwrap_or(os_profile.governance_policy);
870            kernel_apply(
871                &mut kernel,
872                &mut pending_observations,
873                governance_policy.into_kernel_event(),
874            );
875
876            let signal_policy = self
877                .opts
878                .signal_policy
879                .unwrap_or(os_profile.signal_policy);
880            kernel_apply(
881                &mut kernel,
882                &mut pending_observations,
883                KernelInputEvent::SetSignalPolicy {
884                    policy: signal_policy.into_kernel(),
885                },
886            );
887
888            if let Some(quota) = self.opts.resource_quota.clone() {
889                kernel_apply(
890                    &mut kernel,
891                    &mut pending_observations,
892                    KernelInputEvent::SetResourceQuota { quota },
893                );
894            }
895
896            if let Some(policy) = self.opts.memory_policy.clone() {
897                kernel_apply(
898                    &mut kernel,
899                    &mut pending_observations,
900                    memory_policy_event(policy),
901                );
902            }
903
904            // Multimodal upload: seed attachments before start_run (parity with Node/Python).
905            if !resume_mid_run && !attachments.is_empty() {
906                kernel_apply(
907                    &mut kernel,
908                    &mut pending_observations,
909                    KernelInputEvent::AddHistoryMessage {
910                        message: Message::user_multimodal(attachments.clone()),
911                        tokens: None,
912                    },
913                );
914            }
915
916            // I4: pre-fetch memory into the knowledge partition before the first LLM turn.
917            // Mirrors Node/WASM/Python preQueryMemory. Errs-open: missing dream_store/agent_id
918            // or a faulty closure silently skip the pre-fetch.
919            if !resume_mid_run {
920                if let (Some(pre), Some(store), Some(agent_id)) = (
921                    self.opts.pre_query_memory.clone(),
922                    self.opts.dream_store.as_ref(),
923                    self.opts.agent_id.as_deref(),
924                ) {
925                    let queries = pre(goal.as_str());
926                    let mut recalled = Vec::new();
927                    for q in &queries {
928                        if q.query.trim().is_empty() {
929                            continue;
930                        }
931                        if let Ok(hits) = store.search(agent_id, q).await {
932                            for hit in hits {
933                                recalled.push(format!(
934                                    "[memory record_id={} trust={} score={:.3}] {}",
935                                    hit.record.record_id,
936                                    match hit.record.provenance.trust {
937                                        MemoryTrustLevel::Untrusted => "untrusted",
938                                        MemoryTrustLevel::UserAsserted => "user_asserted",
939                                        MemoryTrustLevel::HostVerified => "host_verified",
940                                    },
941                                    hit.score,
942                                    hit.record.content
943                                ));
944                            }
945                        }
946                    }
947                    if !recalled.is_empty() {
948                        kernel_apply(
949                            &mut kernel,
950                            &mut pending_observations,
951                            KernelInputEvent::AddHistoryMessage {
952                                message: Message::user(recalled.join("\n")),
953                                tokens: None,
954                            },
955                        );
956                    }
957                }
958            }
959
960            let mut action = if resume_mid_run {
961                kernel_action(
962                    &mut kernel,
963                    &mut pending_observations,
964                    KernelInputEvent::Resume,
965                )
966            } else {
967                // P0-A: fold an explicit `run_spec` and/or the `allowed_tool_ids` profile into the
968                // kernel's `capability_filter` (reuses the existing run_spec wire — no new ABI).
969                let run_spec = build_run_spec(
970                    self.opts.run_spec.clone(),
971                    self.opts.allowed_tool_ids.as_deref(),
972                    self.opts.agent_id.as_deref(),
973                    &session_id,
974                    &goal,
975                );
976                kernel_action(
977                    &mut kernel,
978                    &mut pending_observations,
979                    KernelInputEvent::StartRun {
980                        task: RuntimeTask::new(&goal).with_criteria(criteria),
981                        run_spec,
982                    },
983                )
984            };
985
986            let mut last_skill_version: u64 = skill_watcher.as_ref().map(|w| w.version()).unwrap_or(0);
987
988            while !kernel.lock().unwrap().is_terminal() {
989                // Hot-reload: refresh skill catalog if the watcher detected changes.
990                if let (Some(watcher), Some(skill_dir)) =
991                    (&skill_watcher, &self.opts.skill_dir)
992                {
993                    let cur = watcher.version();
994                    if cur != last_skill_version {
995                        last_skill_version = cur;
996                        kernel_apply(
997                            &mut kernel,
998                            &mut pending_observations,
999                            KernelInputEvent::SetAvailableSkills {
1000                                skills: scan_skill_dir(skill_dir),
1001                            },
1002                        );
1003                    }
1004                }
1005
1006                next_archive_start = self
1007                    .append_observations(
1008                        &session_id,
1009                        &kernel,
1010                        &mut pending_observations,
1011                        &mut pending_page_out_starts,
1012                        next_archive_start,
1013                    )
1014                    .await;
1015
1016                if self.interrupted.load(Ordering::Relaxed) {
1017                    kernel_apply(
1018                        &mut kernel,
1019                        &mut pending_observations,
1020                        KernelInputEvent::CancelOperation {
1021                            operation_id: "local-operation".into(),
1022                            reason: cancellation_reason_from_code(self.cancellation_reason.load(Ordering::Relaxed)),
1023                            pending_call_ids: pending_call_ids(&action),
1024                        },
1025                    );
1026                    break;
1027                }
1028
1029                if let Some(ss) = &self.opts.signal_source {
1030                    if let Some(claim) = ss.claim_signal().await? {
1031                        let urgency = match claim.signal.urgency.as_str() {
1032                            "low" => Urgency::Low,
1033                            "high" => Urgency::High,
1034                            "critical" => Urgency::Critical,
1035                            _ => Urgency::Normal,
1036                        };
1037                        let source = match claim.signal.source.as_str() {
1038                            "cron" => KernelSignalSource::Cron,
1039                            "gateway" => KernelSignalSource::Gateway,
1040                            "heartbeat" => KernelSignalSource::Heartbeat,
1041                            _ => KernelSignalSource::Custom,
1042                        };
1043                        let signal_type = match claim.signal.signal_type.as_str() {
1044                            "job" => KernelSignalType::Job,
1045                            "alert" => KernelSignalType::Alert,
1046                            _ => KernelSignalType::Event,
1047                        };
1048                        let summary = claim
1049                            .signal
1050                            .payload
1051                            .get("goal")
1052                            .and_then(serde_json::Value::as_str)
1053                            .unwrap_or("signal");
1054                        let mut kernel_sig = KernelSignal::new(
1055                            source,
1056                            signal_type,
1057                            urgency,
1058                            summary,
1059                        )
1060                        .with_payload(claim.signal.payload.clone())
1061                        .with_timestamp(
1062                            std::time::SystemTime::now()
1063                                .duration_since(std::time::UNIX_EPOCH)
1064                                .unwrap_or_default()
1065                                .as_millis() as u64,
1066                        );
1067                        kernel_sig.id = uuid::Uuid::parse_str(&claim.signal_id).map_err(|_| {
1068                            crate::Error::Other("signal claim requires a UUID signal_id".into())
1069                        })?;
1070                        if let Some(dedupe_key) = &claim.signal.dedupe_key {
1071                            kernel_sig = kernel_sig.with_dedupe(dedupe_key.clone());
1072                        }
1073                        if let Some(recipient) = &claim.signal.recipient {
1074                            kernel_sig = kernel_sig.with_recipient(recipient.clone());
1075                        }
1076                        if let Some(deadline_ms) = claim.signal.deadline_ms {
1077                            kernel_sig = kernel_sig.with_deadline(deadline_ms);
1078                        }
1079                        if let Some(coalesce_key) = &claim.signal.coalesce_key {
1080                            kernel_sig = kernel_sig.with_coalesce(coalesce_key.clone());
1081                        }
1082                        kernel_sig.coalesced_count = claim.signal.coalesced_count.max(1);
1083                        // Kernel-routed (parity with node/py): the kernel's attention policy decides
1084                        // the disposition (dedup / queue / interrupt / preempt) and emits
1085                        // `signal_delivery_disposed`; an actionable disposition yields the next action to
1086                        // adopt (e.g. a forced Reason turn on Critical), queued/observed yields none.
1087                        let mut kguard = kernel.lock().unwrap();
1088                        let mut step = kguard.step(KernelInput::new(KernelInputEvent::DeliverSignal {
1089                            delivery_id: claim.delivery_id.clone(),
1090                            attempt: claim.delivery_attempt,
1091                            signal: kernel_sig,
1092                        }));
1093                        drop(kguard);
1094                        let disposition_matches = step.observations.iter().filter(|observation| matches!(
1095                            observation,
1096                            KernelObservation::SignalDeliveryDisposed {
1097                                delivery_id,
1098                                attempt,
1099                                ..
1100                            } if delivery_id == &claim.delivery_id && *attempt == claim.delivery_attempt
1101                        )).count() == 1;
1102                        let receipt = SignalDeliveryReceipt {
1103                            delivery_id: claim.delivery_id,
1104                            lease_token: claim.lease_token,
1105                        };
1106                        if !step.faults.is_empty() || !disposition_matches {
1107                            let _ = ss.nack_signal(&receipt).await?;
1108                            Err(crate::Error::Other(
1109                                "kernel did not return the matching signal delivery disposition".into(),
1110                            ))?;
1111                        }
1112                        if !ss.ack_signal(&receipt).await? {
1113                            let _ = ss.nack_signal(&receipt).await?;
1114                            Err(crate::Error::Other(
1115                                "signal lease was lost before acknowledgement".into(),
1116                            ))?;
1117                        }
1118                        pending_observations.append(&mut step.observations);
1119                        if let Some(sig_action) = step.actions.pop() {
1120                            action = sig_action;
1121                        }
1122                        // Critical attention/preemption is distinct from operation cancellation.
1123                    }
1124                }
1125                if kernel.lock().unwrap().is_terminal() {
1126                    break;
1127                }
1128
1129                match &action.effect {
1130                    KernelEffect::CallProvider { context, tools } => {
1131                        let provider_effect_id = action.effect_id.clone();
1132                        let mut final_text = String::new();
1133                        let mut final_tool_calls: Vec<ToolCall> = Vec::new();
1134                        let mut turn_tokens: u32 = 0;
1135                        let mut turn_input_tokens: u32 = 0;
1136                        let mut turn_cache_read_tokens: u32 = 0;
1137                        let mut turn_cache_creation_tokens: u32 = 0;
1138                        let mut turn_cache_read_by_slot: Option<crate::providers::CacheReadBySlot> = None;
1139                        let mut turn_stop_reason: Option<String> = None;
1140                        // I5: governance schema-level pre-filter. When a GovernancePolicy is loaded
1141                        // and `surface_denied_in_system` is true (default), drop denied tools from
1142                        // the schema before the provider sees them.
1143                        let (filtered_tools, filtered_context_storage);
1144                        let (provider_tools, provider_context): (&[_], &_) = if let Some(policy) = self.opts.governance_policy.as_ref() {
1145                            if policy.surface_denied_in_system {
1146                                let (allowed, denied) = crate::runtime::governance_filter_schema(tools, policy);
1147                                if !denied.is_empty() {
1148                                    filtered_tools = allowed;
1149                                    let mut cloned = context.clone();
1150                                    let note = format!("[governance] the following tools are denied for this run and will fail if called: {}.", denied.join(", "));
1151                                    cloned.system_knowledge = if cloned.system_knowledge.is_empty() {
1152                                        note
1153                                    } else {
1154                                        format!("{}\n\n{}", cloned.system_knowledge, note)
1155                                    };
1156                                    filtered_context_storage = cloned;
1157                                    (&filtered_tools[..], &filtered_context_storage)
1158                                } else {
1159                                    (&tools[..], context)
1160                                }
1161                            } else { (&tools[..], context) }
1162                        } else { (&tools[..], context) };
1163                        // P0-C: snapshot the exposed-tool count now — `tools` borrows `action`, which is
1164                        // reassigned before the metrics emit below.
1165                        let tools_exposed = provider_tools.len();
1166
1167                        let mut provider_stream = match self
1168                            .opts
1169                            .provider
1170                            .stream(provider_context, provider_tools, ext.as_ref(), provider_state.as_ref())
1171                            .await
1172                        {
1173                            Ok(s) => s,
1174                            Err(e) => {
1175                                // Reactive recovery is now a kernel decision. Forward the raw
1176                                // provider error and dispatch whatever the kernel returns:
1177                                // CallProvider to retry with a freshly compacted context, or Done to
1178                                // terminate with an honest ContextOverflow. The classify + compact +
1179                                // retry + give-up policy lives in the kernel (one place), not
1180                                // duplicated across the four SDK runners.
1181                                let msg = e.to_string();
1182                                action = kernel_action(
1183                                    &mut kernel,
1184                                    &mut pending_observations,
1185                                    KernelInputEvent::ProviderError {
1186                                        effect_id: provider_effect_id.clone(),
1187                                        message: msg.clone(),
1188                                    },
1189                                );
1190                                // Withholding (query.ts parity): surface the raw provider error only
1191                                // when the kernel could NOT recover (it returned a terminal). On a
1192                                // recovered retry (CallProvider) the error stays hidden. `continue`
1193                                // re-enters the loop: a recovered turn persists its compaction
1194                                // archive at the loop's normal append point, and a terminal Done
1195                                // exits through `is_terminal()` into the run_terminal emit.
1196                                if matches!(&action.effect, KernelEffect::Done { .. }) {
1197                                    yield RunEvent::Error(msg);
1198                                }
1199                                continue;
1200                            }
1201                        };
1202
1203                        while let Some(evt) = provider_stream.next().await {
1204                            if self.interrupted.load(Ordering::Relaxed) {
1205                                break;
1206                            }
1207                            match evt? {
1208                                StreamEvent::TextDelta { delta } => {
1209                                    final_text.push_str(&delta);
1210                                    yield RunEvent::TextDelta(delta);
1211                                }
1212                                StreamEvent::ThinkingDelta { delta } => {
1213                                    yield RunEvent::ThinkingDelta(delta);
1214                                }
1215                                StreamEvent::ToolCall { id, name, arguments } => {
1216                                    yield RunEvent::ToolCall { id: id.clone(), name: name.clone() };
1217                                    final_tool_calls.push(ToolCall {
1218                                        id: compact_str::CompactString::new(&id),
1219                                        name: compact_str::CompactString::new(&name),
1220                                        arguments,
1221                                    });
1222                                }
1223                                StreamEvent::Usage {
1224                                    total_tokens,
1225                                    input_tokens,
1226                                    cache_read_input_tokens,
1227                                    cache_creation_input_tokens,
1228                                    cache_read_input_tokens_by_slot,
1229                                    stop_reason,
1230                                    ..
1231                                } => {
1232                                    turn_tokens = total_tokens;
1233                                    // P0-C: capture input + prompt-cache split for the hit-rate baseline.
1234                                    turn_input_tokens = input_tokens;
1235                                    turn_cache_read_tokens = cache_read_input_tokens;
1236                                    turn_cache_creation_tokens = cache_creation_input_tokens;
1237                                    turn_cache_read_by_slot = cache_read_input_tokens_by_slot;
1238                                    // Phase 4: keep the last non-empty stop_reason for output-cap recovery.
1239                                    if stop_reason.is_some() { turn_stop_reason = stop_reason; }
1240                                }
1241                                StreamEvent::Done => {}
1242                            }
1243                        }
1244
1245                        if self.interrupted.load(Ordering::Relaxed) {
1246                            action = kernel_action(
1247                                &mut kernel,
1248                                &mut pending_observations,
1249                                KernelInputEvent::CancelOperation {
1250                                    operation_id: "local-operation".into(),
1251                                    reason: cancellation_reason_from_code(self.cancellation_reason.load(Ordering::Relaxed)),
1252                                    pending_call_ids: vec![provider_effect_id],
1253                                },
1254                            );
1255                            break;
1256                        }
1257
1258                        let mut assistant = Message {
1259                            role: deepstrike_core::types::message::Role::Assistant,
1260                            content: deepstrike_core::types::message::Content::Text(final_text.clone()),
1261                            tool_calls: final_tool_calls.clone(),
1262                            token_count: if turn_tokens > 0 { Some(turn_tokens) } else { None },
1263                        };
1264
1265                        self.opts.provider.commit_stream_replay(&final_text, &final_tool_calls);
1266                        let mut provider_replay = peek_provider_replay(
1267                            self.opts.provider.as_ref(),
1268                            &final_text,
1269                            &final_tool_calls,
1270                        );
1271                        repair_llm_completed(&mut assistant, &mut provider_replay);
1272
1273                        action = kernel_action(
1274                            &mut kernel,
1275                            &mut pending_observations,
1276                            KernelInputEvent::ProviderResult {
1277                                effect_id: provider_effect_id,
1278                                message: assistant.clone(),
1279                                observed_input_tokens: None,
1280                                observed_output_tokens: None,
1281                                // COMPAT(gov-clock): rust SDK does not yet drive the in-kernel
1282                                // governance gate, so no clock is fed. Set once it adopts governancePolicy.
1283                                now_ms: None,
1284                                // Phase 4: stop_reason drives the kernel's max-output-tokens recovery.
1285                                stop_reason: turn_stop_reason.clone(),
1286                            },
1287                        );
1288                        self.log(
1289                            &session_id,
1290                            SessionEvent::LlmCompleted {
1291                                turn: kernel.lock().unwrap().turn(),
1292                                message: assistant,
1293                                provider_replay,
1294                            },
1295                        )
1296                        .await;
1297
1298                        // P0-C: per-turn tool-gating telemetry. `active_skill` reflects the skill in
1299                        // effect GOING INTO this turn; a `skill` call here only takes effect next turn
1300                        // — emit first, then advance.
1301                        if let Some(ref sink) = self.opts.on_turn_metrics {
1302                            sink(TurnMetrics {
1303                                turn: kernel.lock().unwrap().turn(),
1304                                tools_exposed,
1305                                tools_called: final_tool_calls.len(),
1306                                active_skill: active_skill.clone(),
1307                                input_tokens: turn_input_tokens,
1308                                cache_read_tokens: turn_cache_read_tokens,
1309                                cache_creation_tokens: turn_cache_creation_tokens,
1310                                cache_read_tokens_by_slot: turn_cache_read_by_slot.clone(),
1311                            });
1312                        }
1313                        if let Some(skill_call) =
1314                            final_tool_calls.iter().find(|c| c.name.as_str() == "skill")
1315                        {
1316                            if let Some(name) = skill_call.arguments.get("name").and_then(|v| v.as_str()) {
1317                                active_skill = Some(name.to_string());
1318                            }
1319                        }
1320                    }
1321                    KernelEffect::RequestApproval { requests } => {
1322                        let approval_effect_id = action.effect_id.clone();
1323                        let mut approved_calls = Vec::new();
1324                        let mut denied_calls = Vec::new();
1325                        for request in requests {
1326                            let arguments = request.arguments.to_string();
1327                            self.log(
1328                                &session_id,
1329                                SessionEvent::PermissionRequested {
1330                                    turn: kernel.lock().unwrap().turn(),
1331                                    tool: request.tool.clone(),
1332                                    arguments: arguments.clone(),
1333                                    reason: Some(request.reason.clone()),
1334                                },
1335                            )
1336                            .await;
1337                            yield RunEvent::PermissionRequest {
1338                                call_id: request.call_id.clone(),
1339                                tool_name: request.tool.clone(),
1340                                arguments: arguments.clone(),
1341                                reason: request.reason.clone(),
1342                            };
1343
1344                            let response = match &self.opts.on_permission_request {
1345                                Some(handler) => match handler(PermissionRequest {
1346                                    call_id: request.call_id.clone(),
1347                                    tool_name: request.tool.clone(),
1348                                    arguments,
1349                                    reason: request.reason.clone(),
1350                                })
1351                                .await
1352                                {
1353                                    Ok(response) => response,
1354                                    Err(err) => PermissionResponse {
1355                                        approved: false,
1356                                        responder: "permission_handler".to_string(),
1357                                        reason: Some(format!("permission handler failed: {err}")),
1358                                    },
1359                                },
1360                                None => PermissionResponse {
1361                                    approved: false,
1362                                    responder: "policy_gate".to_string(),
1363                                    reason: Some("no permission handler configured".to_string()),
1364                                },
1365                            };
1366                            if response.approved {
1367                                approved_calls.push(request.call_id.clone());
1368                            } else {
1369                                denied_calls.push(request.call_id.clone());
1370                            }
1371                            let responder = if response.responder.is_empty() {
1372                                "host".to_string()
1373                            } else {
1374                                response.responder
1375                            };
1376                            self.log(
1377                                &session_id,
1378                                SessionEvent::PermissionResolved {
1379                                    turn: kernel.lock().unwrap().turn(),
1380                                    approved: response.approved,
1381                                    responder: responder.clone(),
1382                                },
1383                            )
1384                            .await;
1385                            yield RunEvent::PermissionResolved {
1386                                call_id: request.call_id.clone(),
1387                                tool_name: request.tool.clone(),
1388                                approved: response.approved,
1389                                responder,
1390                                reason: response.reason,
1391                            };
1392                        }
1393                        action = kernel_action(
1394                            &mut kernel,
1395                            &mut pending_observations,
1396                            KernelInputEvent::ApprovalResult {
1397                                effect_id: approval_effect_id,
1398                                approved_calls,
1399                                denied_calls,
1400                                error: None,
1401                            },
1402                        );
1403                    }
1404                    KernelEffect::SpawnWorkflow { nodes, .. } => {
1405                        // This runner has no workflow child orchestrator. Report each
1406                        // requested spawn as a completed failure instead of treating the
1407                        // action as an observation or leaving the effect unresolved.
1408                        let workflow_effect_id = action.effect_id.clone();
1409                        let failures = nodes
1410                            .into_iter()
1411                            .map(|node| deepstrike_core::runtime::kernel::WorkflowSpawnFailure {
1412                                agent_id: node.agent_id.clone(),
1413                                error: "Rust RuntimeRunner has no workflow orchestrator".to_string(),
1414                            })
1415                            .collect();
1416                        action = kernel_action(
1417                            &mut kernel,
1418                            &mut pending_observations,
1419                            KernelInputEvent::WorkflowSpawnResult {
1420                                effect_id: workflow_effect_id,
1421                                started_agent_ids: Vec::new(),
1422                                failures,
1423                                error: None,
1424                            },
1425                        );
1426                    }
1427                    KernelEffect::PreemptSubAgents { .. } => {
1428                        // RuntimeRunner does not launch external child runners, so
1429                        // there is no host process to cancel before acknowledging.
1430                        let preempt_effect_id = action.effect_id.clone();
1431                        action = kernel_action(
1432                            &mut kernel,
1433                            &mut pending_observations,
1434                            KernelInputEvent::PreemptResult {
1435                                effect_id: preempt_effect_id,
1436                                error: None,
1437                            },
1438                        );
1439                    }
1440                    KernelEffect::PersistMemory { .. } => {
1441                        let effect_id = action.effect_id.clone();
1442                        action = kernel_action(
1443                            &mut kernel,
1444                            &mut pending_observations,
1445                            KernelInputEvent::MemoryPersistResult {
1446                                effect_id,
1447                                error: Some(
1448                                    "memory effects must be executed by RuntimeRunner::write_memory"
1449                                        .to_string(),
1450                                ),
1451                            },
1452                        );
1453                    }
1454                    KernelEffect::QueryMemory { .. } => {
1455                        let effect_id = action.effect_id.clone();
1456                        action = kernel_action(
1457                            &mut kernel,
1458                            &mut pending_observations,
1459                            KernelInputEvent::MemoryQueryResult {
1460                                effect_id,
1461                                hits: Vec::new(),
1462                                error: Some(
1463                                    "memory effects must be executed by RuntimeRunner::query_memory"
1464                                        .to_string(),
1465                                ),
1466                            },
1467                        );
1468                    }
1469                    KernelEffect::SpoolLargeResult { call_id, output, .. } => {
1470                        let effect_id = action.effect_id.clone();
1471                        let spool_dir = self.opts.spool_dir.as_deref().unwrap_or_else(|| std::path::Path::new(".spool"));
1472                        let (spool_ref, error) = match crate::runtime::large_result_spool::persist_output(spool_dir, call_id, output) {
1473                            Ok(path) => (Some(path), None),
1474                            Err(error) => (None, Some(error.to_string())),
1475                        };
1476                        action = kernel_action(
1477                            &mut kernel,
1478                            &mut pending_observations,
1479                            KernelInputEvent::LargeResultSpoolResult { effect_id, spool_ref, error },
1480                        );
1481                    }
1482                    KernelEffect::ArchivePageOut { archived, tier, action: pressure_action, .. } => {
1483                        let effect_id = action.effect_id.clone();
1484                        let archived = archived.clone();
1485                        let tier = tier.clone();
1486                        let action_name = action_str_of(*pressure_action);
1487                        let archive_start = *active_page_out_start.get_or_insert_with(|| {
1488                            pending_page_out_starts.pop_front().unwrap_or(next_archive_start)
1489                        });
1490                        let archive_result = if let Some(store) = &self.opts.compression_store {
1491                            store.write(&session_id, archive_start, &archived)
1492                                .map(|path| (!path.is_empty()).then_some(path))
1493                        } else {
1494                            Ok(None)
1495                        };
1496                        let (archive_ref, error) = match archive_result {
1497                            Ok(archive_ref) => {
1498                                self.local_page_out_cache.lock().unwrap().extend(archived.clone());
1499                                if tier == "semantic" {
1500                                    self.archive_semantic_page_out(archived, Some(action_name)).await;
1501                                }
1502                                (archive_ref, None)
1503                            }
1504                            Err(error) => (None, Some(error.to_string())),
1505                        };
1506                        if error.is_none() {
1507                            active_page_out_start = None;
1508                        }
1509                        action = kernel_action(
1510                            &mut kernel,
1511                            &mut pending_observations,
1512                            KernelInputEvent::PageOutArchiveResult { effect_id, archive_ref, error },
1513                        );
1514                    }
1515                    KernelEffect::ExecuteTool { calls } => {
1516                        let tool_effect_id = action.effect_id.clone();
1517                        let tool_calls = calls.clone();
1518                        self.log(
1519                            &session_id,
1520                            SessionEvent::ToolRequested {
1521                                turn: kernel.lock().unwrap().turn(),
1522                                calls: tool_calls.clone(),
1523                            },
1524                        )
1525                        .await;
1526
1527                        if let Some(gov) = &self.opts.governance {
1528                            let mut g = gov.lock().await;
1529                            if let Some(aid) = &self.opts.agent_id {
1530                                g.set_identity(aid, &session_id);
1531                            }
1532                        }
1533
1534                        let run_ctx = RunContext {
1535                            agent_id: self.opts.agent_id.as_deref(),
1536                            memory_scope: self.opts.memory_scope.as_ref(),
1537                            skill_dir: self.opts.skill_dir.as_deref(),
1538                            dream_store: self.opts.dream_store.as_deref(),
1539                            knowledge_source: self.opts.knowledge_source.as_deref(),
1540                            governance: self.opts.governance.clone(),
1541                            on_tool_suspend: self.opts.on_tool_suspend.clone(),
1542                            on_permission_request: self.opts.on_permission_request.clone(),
1543                        };
1544
1545                        let mut tool_results = Vec::new();
1546                        let mut normal_calls = Vec::new();
1547                        let mut plan_calls = Vec::new();
1548
1549                        for call in &tool_calls {
1550                            if call.name == "update_plan" {
1551                                plan_calls.push(call);
1552                            } else {
1553                                normal_calls.push(call.clone());
1554                            }
1555                        }
1556
1557                        for call in plan_calls {
1558                            let update = parse_update_plan_args(&call.arguments);
1559                            kernel_apply(
1560                                &mut kernel,
1561                                &mut pending_observations,
1562                                KernelInputEvent::UpdateTask { update },
1563                            );
1564                            tool_results.push(deepstrike_core::types::message::ToolResult {
1565                                call_id: call.id.clone(),
1566                                output: deepstrike_core::types::message::Content::Text("success".to_string()),
1567                                is_error: false,
1568                                is_fatal: false,
1569                                error_kind: None,
1570                                token_count: None,
1571                            });
1572                            yield RunEvent::ToolResult {
1573                                call_id: call.id.to_string(),
1574                                content: "success".to_string(),
1575                                is_error: false,
1576                                is_fatal: false,
1577                                error_kind: None,
1578                            };
1579                        }
1580
1581                        if !normal_calls.is_empty() {
1582                            let plane_stream = self.plane.execute_all(&normal_calls, run_ctx);
1583                            let mut stream = plane_stream;
1584                            while let Some(evt) = stream.next().await {
1585                                match evt? {
1586                                    RunEvent::ToolResult {
1587                                        call_id,
1588                                        content,
1589                                        is_error,
1590                                        is_fatal,
1591                                        error_kind,
1592                                    } => {
1593                                        tool_results.push(deepstrike_core::types::message::ToolResult {
1594                                            call_id: compact_str::CompactString::new(&call_id),
1595                                            output: deepstrike_core::types::message::Content::Text(content),
1596                                            is_error,
1597                                            is_fatal,
1598                                            error_kind,
1599                                            token_count: None,
1600                                        });
1601                                    }
1602                                    RunEvent::ToolArgumentRepaired { call_id, name, original_arguments, repaired_arguments } => {
1603                                        self.log(
1604                                            &session_id,
1605                                            SessionEvent::ToolArgumentRepaired {
1606                                                turn: kernel.lock().unwrap().turn(),
1607                                                tool: name.clone(),
1608                                                original_arguments: original_arguments.clone(),
1609                                                repaired_arguments: repaired_arguments.clone(),
1610                                            },
1611                                        )
1612                                        .await;
1613                                        yield RunEvent::ToolArgumentRepaired {
1614                                            call_id,
1615                                            name,
1616                                            original_arguments,
1617                                            repaired_arguments,
1618                                        };
1619                                    }
1620                                    RunEvent::ToolDenied { call_id, tool_name, reason } => {
1621                                        self.log(
1622                                            &session_id,
1623                                            SessionEvent::ToolDenied {
1624                                                turn: kernel.lock().unwrap().turn(),
1625                                                call_id: call_id.clone(),
1626                                                tool_name: tool_name.clone(),
1627                                                reason: reason.clone(),
1628                                            },
1629                                        )
1630                                        .await;
1631                                        yield RunEvent::ToolDenied { call_id, tool_name, reason };
1632                                    }
1633                                    RunEvent::PermissionRequest { call_id, tool_name, arguments, reason } => {
1634                                        let turn = kernel.lock().unwrap().turn();
1635                                        self.log(
1636                                            &session_id,
1637                                            SessionEvent::PermissionRequested {
1638                                                turn,
1639                                                tool: tool_name.clone(),
1640                                                arguments: arguments.clone(),
1641                                                reason: Some(reason.clone()),
1642                                            },
1643                                        )
1644                                        .await;
1645                                        yield RunEvent::PermissionRequest { call_id, tool_name, arguments, reason };
1646                                    }
1647                                    RunEvent::PermissionResolved { call_id, tool_name, approved, responder, reason } => {
1648                                        let turn = kernel.lock().unwrap().turn();
1649                                        self.log(
1650                                            &session_id,
1651                                            SessionEvent::PermissionResolved {
1652                                                turn,
1653                                                approved,
1654                                                responder: responder.clone(),
1655                                            },
1656                                        )
1657                                        .await;
1658                                        yield RunEvent::PermissionResolved { call_id, tool_name, approved, responder, reason };
1659                                    }
1660                                    other => yield other,
1661                                }
1662                            }
1663                            let names: Vec<String> = normal_calls.iter().map(|c| c.name.to_string()).collect();
1664                            kernel_apply(
1665                                &mut kernel,
1666                                &mut pending_observations,
1667                                KernelInputEvent::UpdateTask {
1668                                    update: TaskUpdate {
1669                                        progress: Some(format!("Executed tools: {}", names.join(", "))),
1670                                        ..Default::default()
1671                                    },
1672                                },
1673                            );
1674                        }
1675
1676                        self.log(
1677                            &session_id,
1678                            SessionEvent::ToolCompleted {
1679                                turn: kernel.lock().unwrap().turn(),
1680                                results: tool_results.clone(),
1681                            },
1682                        )
1683                        .await;
1684
1685                        // P1-B B3: a successfully-resolved `skill` call activates that skill for the
1686                        // next turn (fed before ToolResults, which computes the next action).
1687                        for call in &tool_calls {
1688                            if call.name.as_str() != "skill" {
1689                                continue;
1690                            }
1691                            let ok = tool_results
1692                                .iter()
1693                                .any(|r| r.call_id.as_str() == call.id.as_str() && !r.is_error);
1694                            if !ok {
1695                                continue;
1696                            }
1697                            if let Some(name) = call.arguments.get("name").and_then(|v| v.as_str()) {
1698                                kernel_apply(
1699                                    &mut kernel,
1700                                    &mut pending_observations,
1701                                    KernelInputEvent::SkillActivated { name: name.to_string(), lease_turns: None },
1702                                );
1703                            }
1704                        }
1705
1706                        action = kernel_action(
1707                            &mut kernel,
1708                            &mut pending_observations,
1709                            KernelInputEvent::ToolResults {
1710                                effect_id: tool_effect_id,
1711                                results: tool_results,
1712                            },
1713                        );
1714                    }
1715                    KernelEffect::EvaluateMilestone {
1716                        phase_id,
1717                        criteria,
1718                        required_evidence,
1719                        ..
1720                    } => {
1721                        let milestone_effect_id = action.effect_id.clone();
1722                        let policy = self.opts.milestone_policy;
1723                        if policy == MilestonePolicy::AutoPass {
1724                            let result = MilestoneCheckResult::pass(phase_id.clone());
1725                            action = kernel_action(
1726                                &mut kernel,
1727                                &mut pending_observations,
1728                                KernelInputEvent::MilestoneResult {
1729                                    effect_id: milestone_effect_id.clone(),
1730                                    result,
1731                                },
1732                            );
1733                            next_archive_start = self
1734                                .append_observations(
1735                                    &session_id,
1736                                    &kernel,
1737                                    &mut pending_observations,
1738                                    &mut pending_page_out_starts,
1739                                    next_archive_start,
1740                                )
1741                                .await;
1742                        } else if let Some(handler) = &self.opts.on_milestone_evaluate {
1743                            let context = MilestoneEvaluationContext {
1744                                phase_id: phase_id.clone(),
1745                                criteria: criteria.clone(),
1746                                required_evidence: required_evidence.clone(),
1747                            };
1748                            let check_future = handler(context);
1749                            let result = check_future.await?;
1750                            action = kernel_action(
1751                                &mut kernel,
1752                                &mut pending_observations,
1753                                KernelInputEvent::MilestoneResult {
1754                                    effect_id: milestone_effect_id,
1755                                    result,
1756                                },
1757                            );
1758                            next_archive_start = self
1759                                .append_observations(
1760                                    &session_id,
1761                                    &kernel,
1762                                    &mut pending_observations,
1763                                    &mut pending_page_out_starts,
1764                                    next_archive_start,
1765                                )
1766                                .await;
1767                        } else {
1768                            next_archive_start = self
1769                                .append_observations(
1770                                    &session_id,
1771                                    &kernel,
1772                                    &mut pending_observations,
1773                                    &mut pending_page_out_starts,
1774                                    next_archive_start,
1775                                )
1776                                .await;
1777                            self.log(
1778                                &session_id,
1779                                SessionEvent::RunTerminal {
1780                                    reason: "milestone_pending".to_string(),
1781                                    turns_used: kernel.lock().unwrap().turn().max(1),
1782                                    total_tokens: 0,
1783                                },
1784                            )
1785                            .await;
1786                            yield RunEvent::Done {
1787                                iterations: kernel.lock().unwrap().turn().max(1),
1788                                total_tokens: 0,
1789                                status: "milestone_pending".to_string(),
1790                            };
1791                            return;
1792                        }
1793                    }
1794                    KernelEffect::Done { result } => {
1795                        let status = format!("{:?}", result.termination).to_lowercase();
1796                        let turns_used = result.turns_used.max(1);
1797                        let total_tokens = result.total_tokens_used;
1798
1799                        next_archive_start = self
1800                            .append_observations(
1801                                &session_id,
1802                                &kernel,
1803                                &mut pending_observations,
1804                                &mut pending_page_out_starts,
1805                                next_archive_start,
1806                            )
1807                            .await;
1808
1809                        self.log(
1810                            &session_id,
1811                            SessionEvent::RunTerminal {
1812                                reason: status.clone(),
1813                                turns_used,
1814                                total_tokens,
1815                            },
1816                        )
1817                        .await;
1818
1819                        if let (Some(store), Some(agent_id)) =
1820                            (&self.opts.dream_store, &self.opts.agent_id)
1821                        {
1822                            let new_msgs = kernel.lock().unwrap().drain_new_messages();
1823                            if !new_msgs.is_empty() {
1824                                let now_ms = std::time::SystemTime::now()
1825                                    .duration_since(std::time::UNIX_EPOCH)
1826                                    .unwrap_or_default()
1827                                    .as_millis() as u64;
1828                                let session = deepstrike_core::memory::durable::SessionData {
1829                                    session_id: session_id.clone(),
1830                                    agent_id: agent_id.clone(),
1831                                    messages: new_msgs,
1832                                    metadata: serde_json::Value::Null,
1833                                    created_at_ms: session_start_ms,
1834                                    updated_at_ms: now_ms,
1835                                };
1836                                let _ = store.save_session(session.clone()).await;
1837                                if let Some(scope) = self.opts.memory_scope.as_ref() {
1838                                    if let Ok(memories) = self.extract_session_memories(&session, scope).await {
1839                                        for memory in memories {
1840                                            let _ = self.write_memory(memory, Some(&session_id), Some(agent_id)).await;
1841                                        }
1842                                    }
1843                                }
1844                            }
1845                        }
1846
1847                        yield RunEvent::Done {
1848                            iterations: turns_used,
1849                            total_tokens,
1850                            status,
1851                        };
1852                        return;
1853                    }
1854                }
1855            }
1856
1857            next_archive_start = self
1858                .append_observations(
1859                    &session_id,
1860                    &kernel,
1861                    &mut pending_observations,
1862                    &mut pending_page_out_starts,
1863                    next_archive_start,
1864                )
1865                .await;
1866
1867            // I0a: when the loop exits without a clean kernel-done, preserve preempt intent
1868            // (interrupted flag set) in the run_terminal reason — otherwise an interrupt-curtailed
1869            // run reports "error" indistinguishable from a real crash. Mirrors Node/WASM/Python.
1870            let (status, turns_used, total_tokens) = match &action.effect {
1871                KernelEffect::Done { result } => (
1872                    format!("{:?}", result.termination).to_lowercase(),
1873                    result.turns_used.max(1),
1874                    result.total_tokens_used,
1875                ),
1876                _ => ("error".to_string(), kernel.lock().unwrap().turn().max(1), 0),
1877            };
1878
1879            self.log(
1880                &session_id,
1881                SessionEvent::RunTerminal {
1882                    reason: status.clone(),
1883                    turns_used,
1884                    total_tokens,
1885                },
1886            )
1887            .await;
1888
1889            if let KernelEffect::Done { .. } = &action.effect {
1890                if let (Some(store), Some(agent_id)) =
1891                    (&self.opts.dream_store, &self.opts.agent_id)
1892                {
1893                    let new_msgs = kernel.lock().unwrap().drain_new_messages();
1894                    if !new_msgs.is_empty() {
1895                        let now_ms = std::time::SystemTime::now()
1896                            .duration_since(std::time::UNIX_EPOCH)
1897                            .unwrap_or_default()
1898                            .as_millis() as u64;
1899                        let session = deepstrike_core::memory::durable::SessionData {
1900                            session_id: session_id.clone(),
1901                            agent_id: agent_id.clone(),
1902                            messages: new_msgs,
1903                            metadata: serde_json::Value::Null,
1904                            created_at_ms: session_start_ms,
1905                            updated_at_ms: now_ms,
1906                        };
1907                        let _ = store.save_session(session.clone()).await;
1908                        if let Some(scope) = self.opts.memory_scope.as_ref() {
1909                            if let Ok(memories) = self.extract_session_memories(&session, scope).await {
1910                                for memory in memories {
1911                                    let _ = self.write_memory(memory, Some(&session_id), Some(agent_id)).await;
1912                                }
1913                            }
1914                        }
1915                    }
1916                }
1917            }
1918
1919            yield RunEvent::Done {
1920                iterations: turns_used,
1921                total_tokens,
1922                status,
1923            };
1924        }
1925    }
1926
1927    pub(crate) async fn append_observations(
1928        &self,
1929        session_id: &str,
1930        kernel_mutex: &std::sync::Mutex<KernelRuntime>,
1931        observations: &mut Vec<KernelObservation>,
1932        pending_page_out_starts: &mut std::collections::VecDeque<u64>,
1933        mut next_archive_start: u64,
1934    ) -> u64 {
1935        let drained = std::mem::take(observations);
1936        let (turn, preserved_refs, summary_tokens_by_index) = {
1937            let kernel = kernel_mutex.lock().unwrap();
1938            let summary_tokens_by_index = drained
1939                .iter()
1940                .map(|obs| match obs {
1941                    KernelObservation::Compressed { summary, .. } => {
1942                        summary.as_ref().map(|s| kernel.count_tokens(s))
1943                    }
1944                    _ => None,
1945                })
1946                .collect::<Vec<_>>();
1947            (
1948                kernel.turn(),
1949                kernel.preserved_refs(),
1950                summary_tokens_by_index,
1951            )
1952        };
1953
1954        for (index, obs) in drained.into_iter().enumerate() {
1955            match obs {
1956                KernelObservation::Compressed {
1957                    turn: _,
1958                    action,
1959                    rho_after: _,
1960                    summary,
1961                    archived_count,
1962                    invalidates_prefix_at: _,
1963                } => {
1964                    let Some(log) = &self.opts.session_log else {
1965                        continue;
1966                    };
1967                    let latest = log.latest_seq(session_id).await.unwrap_or(-1) as u64;
1968                    if latest < next_archive_start {
1969                        continue;
1970                    }
1971                    let end = latest;
1972                    if archived_count > 0 {
1973                        pending_page_out_starts.push_back(next_archive_start);
1974                    }
1975
1976                    let summary_tokens = summary_tokens_by_index.get(index).copied().flatten();
1977                    let action_str = action_str_of(action);
1978
1979                    if let Ok(compressed_seq) = log
1980                        .append(
1981                            session_id,
1982                            SessionEvent::Compressed {
1983                                turn,
1984                                archived_seq_range: (next_archive_start, end),
1985                                action: Some(action_str),
1986                                summary: summary.clone(),
1987                                summary_tokens,
1988                                preserved_refs: preserved_refs.clone(),
1989                            },
1990                        )
1991                        .await
1992                    {
1993                        next_archive_start = compressed_seq + 1;
1994                    }
1995                }
1996                KernelObservation::PageOutArchived {
1997                    turn,
1998                    action,
1999                    summary,
2000                    tier,
2001                    message_count,
2002                    archive_ref,
2003                } => {
2004                    self.log(
2005                        session_id,
2006                        SessionEvent::PageOut {
2007                            turn,
2008                            action: Some(action_str_of(action)),
2009                            summary,
2010                            tier_hint: Some(tier),
2011                            message_count,
2012                            archive_ref,
2013                        },
2014                    )
2015                    .await;
2016                }
2017                KernelObservation::PageOutArchiveFailed { .. } => {}
2018                KernelObservation::Rollbacked {
2019                    turn,
2020                    checkpoint_history_len,
2021                    reason,
2022                } => {
2023                    self.log(
2024                        session_id,
2025                        SessionEvent::Rollbacked {
2026                            turn,
2027                            checkpoint_history_len,
2028                            reason,
2029                        },
2030                    )
2031                    .await;
2032                }
2033                KernelObservation::CapabilityChanged {
2034                    turn,
2035                    added,
2036                    removed,
2037                    change_kind,
2038                    capability_id,
2039                    version,
2040                    mounted_by,
2041                    mount_reason,
2042                } => {
2043                    self.log(
2044                        session_id,
2045                        SessionEvent::CapabilityChanged {
2046                            turn,
2047                            added,
2048                            removed,
2049                            change_kind,
2050                            capability_id,
2051                            version,
2052                            mounted_by,
2053                            mount_reason,
2054                        },
2055                    )
2056                    .await;
2057                }
2058                KernelObservation::MilestoneAdvanced {
2059                    turn,
2060                    phase_id,
2061                    capabilities_unlocked,
2062                } => {
2063                    self.log(
2064                        session_id,
2065                        SessionEvent::MilestoneAdvanced {
2066                            turn,
2067                            phase_id,
2068                            capabilities_unlocked,
2069                        },
2070                    )
2071                    .await;
2072                }
2073                KernelObservation::MilestoneBlocked {
2074                    turn,
2075                    phase_id,
2076                    reason,
2077                } => {
2078                    self.log(
2079                        session_id,
2080                        SessionEvent::MilestoneBlocked {
2081                            turn,
2082                            phase_id,
2083                            reason,
2084                        },
2085                    )
2086                    .await;
2087                }
2088                KernelObservation::Renewed { .. } => {}
2089                KernelObservation::ContextBudgetExceeded { .. } => {}
2090                KernelObservation::KnowledgeSwept { .. } => {}
2091                KernelObservation::KnowledgeBudgetExceeded { .. } => {}
2092                KernelObservation::RepeatFuseTripped { .. } => {}
2093                KernelObservation::CriteriaGateFired { .. } => {}
2094                KernelObservation::CheckpointTaken { turn, history_len } => {
2095                    self.log(
2096                        session_id,
2097                        SessionEvent::CheckpointTaken { turn, history_len },
2098                    )
2099                    .await;
2100                }
2101                KernelObservation::EntropySample {
2102                    turn,
2103                    score,
2104                    score_version,
2105                    rho,
2106                    repeat_pressure,
2107                    failure_rate,
2108                    rollbacks_in_window,
2109                    window_turns,
2110                } => {
2111                    self.log(
2112                        session_id,
2113                        SessionEvent::EntropySample {
2114                            turn,
2115                            score,
2116                            score_version,
2117                            rho,
2118                            repeat_pressure,
2119                            failure_rate,
2120                            rollbacks_in_window,
2121                            window_turns,
2122                        },
2123                    )
2124                    .await;
2125                }
2126                KernelObservation::EntropyAlert {
2127                    turn,
2128                    score,
2129                    threshold,
2130                } => {
2131                    self.log(
2132                        session_id,
2133                        SessionEvent::EntropyAlert {
2134                            turn,
2135                            score,
2136                            threshold,
2137                        },
2138                    )
2139                    .await;
2140                }
2141                KernelObservation::AgentProcessChanged { .. } => {}
2142                // W0-ABI workflow lifecycle. The rust SDK has no workflow drive yet
2143                // (node/python only), so these are observed-but-ignored here.
2144                KernelObservation::WorkflowBatchSpawned { .. } => {}
2145                KernelObservation::WorkflowSpawnFailed { .. } => {}
2146                KernelObservation::WorkflowCompleted { .. } => {}
2147                KernelObservation::NodesRejected { .. } => {}
2148                KernelObservation::AgentPreempted { .. } => {}
2149                KernelObservation::AgentPreemptFailed { .. } => {}
2150                KernelObservation::MemoryWriteFailed { .. } => {}
2151                KernelObservation::MemoryQueryFailed { .. } => {}
2152                // M3/M4 lifecycle observations. Durable-store mirroring is a Node/Python SDK
2153                // concern; this Rust session-log loop does not persist them (parity follow-up).
2154                KernelObservation::MemoryRecalled { .. }
2155                | KernelObservation::PromotionSuggested { .. } => {}
2156                // Governance flagged a tool call for user approval. The kernel does
2157                // not block it; the SDK-side human-approval workflow is a follow-up.
2158                KernelObservation::ToolGated { .. } => {}
2159                // In-kernel signal routing decision. The rust SDK does not yet drive
2160                // signals through the kernel attention policy; observation is logged
2161                // by the generic observation path elsewhere if needed.
2162                KernelObservation::SignalDeliveryDisposed { .. } => {}
2163                KernelObservation::SignalDisplaced { .. }
2164                | KernelObservation::SignalExpired { .. }
2165                | KernelObservation::SignalsPending { .. } => {}
2166                KernelObservation::BudgetExceeded {
2167                    turn,
2168                    operation_id,
2169                    reservation_id,
2170                    budget,
2171                } => {
2172                    self.log(
2173                        session_id,
2174                        SessionEvent::BudgetExceeded {
2175                            turn,
2176                            operation_id,
2177                            reservation_id,
2178                            budget,
2179                        },
2180                    )
2181                    .await;
2182                }
2183                KernelObservation::BudgetUsageReported {
2184                    operation_id,
2185                    reservation_id,
2186                    tokens,
2187                    subagents,
2188                    rounds,
2189                } => {
2190                    self.log(
2191                        session_id,
2192                        SessionEvent::BudgetUsageReported {
2193                            turn,
2194                            operation_id,
2195                            reservation_id,
2196                            tokens,
2197                            subagents,
2198                            rounds,
2199                        },
2200                    )
2201                    .await;
2202                }
2203                KernelObservation::OperationCancelled {
2204                    turn,
2205                    operation_id,
2206                    reason,
2207                    pending_call_ids,
2208                } => {
2209                    self.log(
2210                        session_id,
2211                        SessionEvent::OperationCancelled {
2212                            turn,
2213                            operation_id,
2214                            reason,
2215                            pending_call_ids,
2216                        },
2217                    )
2218                    .await;
2219                }
2220                KernelObservation::Suspended { .. }
2221                | KernelObservation::ApprovalResolutionFailed { .. } => {}
2222                KernelObservation::Resumed { .. } => {}
2223                // R3-1: submission bookkeeping — the rust SDK has no workflow driver, so the
2224                // base-index observation has no session record to enrich here.
2225                KernelObservation::WorkflowNodesSubmitted { .. } => {}
2226                // ③ loop-agent pacing: the rust SDK has no loop driver yet; the decision also
2227                // rides LoopResult.pace_decision for embedders that want it.
2228                KernelObservation::RoundPaced { .. } => {}
2229                KernelObservation::MemoryWritten {
2230                    turn,
2231                    record_id,
2232                    scope,
2233                    memory_kind,
2234                    name,
2235                    size_bytes,
2236                } => {
2237                    self.log(
2238                        session_id,
2239                        SessionEvent::MemoryWritten {
2240                            turn,
2241                            record_id,
2242                            scope,
2243                            memory_kind,
2244                            name,
2245                            size_bytes,
2246                        },
2247                    )
2248                    .await;
2249                }
2250                KernelObservation::MemoryQueried {
2251                    turn,
2252                    scope,
2253                    query,
2254                    requested_k,
2255                    requires_async_response,
2256                } => {
2257                    self.log(
2258                        session_id,
2259                        SessionEvent::MemoryQueried {
2260                            turn,
2261                            scope,
2262                            query,
2263                            requested_k,
2264                            requires_async_response,
2265                        },
2266                    )
2267                    .await;
2268                }
2269                // Phase 7 / M3: no dedicated session kinds yet in rust SDK.
2270                KernelObservation::MemoryValidationFailed {
2271                    turn,
2272                    record_id,
2273                    error,
2274                } => {
2275                    self.log(
2276                        session_id,
2277                        SessionEvent::MemoryValidationFailed {
2278                            turn,
2279                            record_id,
2280                            error,
2281                        },
2282                    )
2283                    .await;
2284                }
2285                KernelObservation::LargeResultSpooled {
2286                    turn,
2287                    call_id,
2288                    tool,
2289                    original_size,
2290                    preview_size,
2291                    spool_ref,
2292                } => {
2293                    self.log(
2294                        session_id,
2295                        SessionEvent::LargeResultSpooled {
2296                            turn,
2297                            call_id,
2298                            tool,
2299                            original_size,
2300                            preview_size,
2301                            spool_ref,
2302                        },
2303                    )
2304                    .await;
2305                }
2306                KernelObservation::LargeResultSpoolFailed { .. } => {}
2307            }
2308        }
2309        next_archive_start
2310    }
2311
2312    async fn read_entries(&self, session_id: &str) -> Result<Vec<SessionEntry>> {
2313        if let Some(log) = &self.opts.session_log {
2314            log.read(session_id, 0, None).await.map_err(Error::Io)
2315        } else {
2316            Ok(Vec::new())
2317        }
2318    }
2319
2320    async fn log(&self, session_id: &str, event: SessionEvent) {
2321        if let Some(log) = &self.opts.session_log {
2322            let _ = log.append(session_id, event).await;
2323        }
2324    }
2325
2326    async fn archive_semantic_page_out(&self, archived: Vec<Message>, action: Option<String>) {
2327        let (Some(_store), Some(agent_id), Some(scope)) = (
2328            &self.opts.dream_store,
2329            &self.opts.agent_id,
2330            &self.opts.memory_scope,
2331        ) else {
2332            return;
2333        };
2334
2335        let summary = match self.summarize_for_long_term_memory(&archived).await {
2336            Ok(s) => s,
2337            Err(_) => return, // non-fatal
2338        };
2339
2340        // P2 write-funnel: route through the ONE gated write_memory syscall so validation,
2341        // the rolling write quota, dedup, and the memory_written audit all apply. Score is
2342        // advisory (0.6) — an automatic summary must never outrank curated content.
2343        let now = std::time::SystemTime::now()
2344            .duration_since(std::time::UNIX_EPOCH)
2345            .unwrap_or_default()
2346            .as_millis() as u64;
2347        let name = format!("page-out-{now}");
2348        let request = MemoryRecord {
2349            record_id: format!("{}:{}:project:{}", scope.tenant_id, scope.namespace, name),
2350            scope: scope.clone(),
2351            name,
2352            kind: MemoryKind::Project,
2353            content: summary,
2354            description: format!(
2355                "auto summary of {} archive",
2356                action.as_deref().unwrap_or("compaction")
2357            ),
2358            provenance: MemoryProvenance {
2359                session_id: self.opts.session_id.clone(),
2360                author: MemoryAuthor::Extraction,
2361                trust: MemoryTrustLevel::Untrusted,
2362                evidence_refs: Vec::new(),
2363            },
2364            created_at: now,
2365            updated_at: now,
2366            last_recalled_at: None,
2367            recall_count: 0,
2368            confidence: 0.6,
2369            links: Vec::new(),
2370            pinned: false,
2371            ttl_days: None,
2372        };
2373        let _ = self.write_memory(request, None, Some(agent_id)).await;
2374    }
2375
2376    async fn summarize_for_long_term_memory(&self, archived: &[Message]) -> crate::Result<String> {
2377        let transcript = archived
2378            .iter()
2379            .map(|m| {
2380                let role_str = match m.role {
2381                    deepstrike_core::types::message::Role::System => "system",
2382                    deepstrike_core::types::message::Role::User => "user",
2383                    deepstrike_core::types::message::Role::Assistant => "assistant",
2384                    deepstrike_core::types::message::Role::Tool => "tool",
2385                };
2386                let content_str = message_content_as_text(&m.content);
2387                format!("{}: {}", role_str, content_str)
2388            })
2389            .collect::<Vec<_>>()
2390            .join("\n");
2391
2392        let system_prompt_opt = self.opts.system_prompt.as_deref();
2393        let system_text = match system_prompt_opt {
2394            Some(sp) => format!(
2395                "{}\n\nSummarize the following conversation for long-term memory. Preserve key facts, decisions, and open questions.",
2396                sp
2397            ),
2398            None => "Summarize the following conversation for long-term memory. Preserve key facts, decisions, and open questions.".to_string(),
2399        };
2400
2401        let context = deepstrike_core::context::renderer::RenderedContext {
2402            system_text,
2403            system_stable: String::new(),
2404            system_knowledge: String::new(),
2405            turns: vec![deepstrike_core::types::message::Message {
2406                role: deepstrike_core::types::message::Role::User,
2407                content: deepstrike_core::types::message::Content::Text(transcript.clone()),
2408                tool_calls: vec![],
2409                token_count: None,
2410            }],
2411            state_turn: None,
2412            frozen_prefix_len: None,
2413            budget_overflow: None,
2414        };
2415
2416        let synth_state = self.opts.provider.create_run_state();
2417        let mut stream = self
2418            .opts
2419            .provider
2420            .stream(&context, &[], None, synth_state.as_ref())
2421            .await?;
2422
2423        let mut synthesis_text = String::new();
2424        while let Some(evt) = stream.next().await {
2425            if let Ok(StreamEvent::TextDelta { delta }) = evt {
2426                synthesis_text.push_str(&delta);
2427            }
2428        }
2429
2430        let text = synthesis_text.trim();
2431        if text.is_empty() {
2432            Ok(transcript.chars().take(2000).collect())
2433        } else {
2434            Ok(text.to_string())
2435        }
2436    }
2437}
2438
2439fn message_content_as_text(content: &deepstrike_core::types::message::Content) -> String {
2440    match content {
2441        deepstrike_core::types::message::Content::Text(s) => s.clone(),
2442        deepstrike_core::types::message::Content::Parts(parts) => parts
2443            .iter()
2444            .filter_map(|p| match p {
2445                deepstrike_core::types::message::ContentPart::Text { text } => Some(text.as_str()),
2446                deepstrike_core::types::message::ContentPart::ToolResult { output, .. } => {
2447                    Some(output.as_str())
2448                }
2449                _ => None,
2450            })
2451            .collect::<Vec<_>>()
2452            .join("\n"),
2453    }
2454}
2455
2456fn action_str_of(action: KernelPressureAction) -> String {
2457    match action {
2458        KernelPressureAction::None => "none".to_string(),
2459        KernelPressureAction::SnipCompact => "snip_compact".to_string(),
2460        KernelPressureAction::MicroCompact => "micro_compact".to_string(),
2461        KernelPressureAction::ContextCollapse => "context_collapse".to_string(),
2462        KernelPressureAction::AutoCompact => "auto_compact".to_string(),
2463    }
2464}
2465
2466fn kernel_apply(
2467    kernel: &mut KernelRuntime,
2468    pending_observations: &mut Vec<KernelObservation>,
2469    event: KernelInputEvent,
2470) {
2471    let step = kernel.step(KernelInput::new(event));
2472    pending_observations.extend(step.observations);
2473}
2474
2475fn kernel_action(
2476    kernel: &mut KernelRuntime,
2477    pending_observations: &mut Vec<KernelObservation>,
2478    event: KernelInputEvent,
2479) -> KernelAction {
2480    let mut step = kernel.step(KernelInput::new(event));
2481    pending_observations.append(&mut step.observations);
2482    take_single_action(step)
2483}
2484
2485fn take_single_action(mut step: KernelStep) -> KernelAction {
2486    step.actions
2487        .pop()
2488        .expect("kernel transition must return one action")
2489}
2490
2491pub async fn collect_text(
2492    mut stream: std::pin::Pin<Box<dyn futures::Stream<Item = Result<RunEvent>> + '_>>,
2493) -> Result<String> {
2494    let mut text = String::new();
2495    while let Some(evt) = stream.next().await {
2496        if let RunEvent::TextDelta(d) = evt? {
2497            text.push_str(&d);
2498        }
2499    }
2500    Ok(text)
2501}
2502
2503fn merge_extensions(
2504    base: Option<&serde_json::Value>,
2505    over: Option<&serde_json::Value>,
2506) -> Option<serde_json::Value> {
2507    match (base, over) {
2508        (Some(b), Some(o)) => {
2509            let mut merged = b.clone();
2510            if let (Some(m), Some(obj)) = (merged.as_object_mut(), o.as_object()) {
2511                for (k, v) in obj {
2512                    m.insert(k.clone(), v.clone());
2513                }
2514            }
2515            Some(merged)
2516        }
2517        (Some(b), None) => Some(b.clone()),
2518        (None, Some(o)) => Some(o.clone()),
2519        (None, None) => None,
2520    }
2521}
2522
2523fn cancellation_reason_code(reason: CancellationReason) -> u8 {
2524    match reason {
2525        CancellationReason::User => 0,
2526        CancellationReason::Deadline => 1,
2527        CancellationReason::LeaseLost => 2,
2528        CancellationReason::HostShutdown => 3,
2529    }
2530}
2531
2532fn cancellation_reason_from_code(code: u8) -> CancellationReason {
2533    match code {
2534        1 => CancellationReason::Deadline,
2535        2 => CancellationReason::LeaseLost,
2536        3 => CancellationReason::HostShutdown,
2537        _ => CancellationReason::User,
2538    }
2539}
2540
2541fn pending_call_ids(action: &KernelAction) -> Vec<String> {
2542    match &action.effect {
2543        KernelEffect::CallProvider { .. } => vec![action.effect_id.clone()],
2544        KernelEffect::ExecuteTool { calls } => {
2545            calls.iter().map(|call| call.id.to_string()).collect()
2546        }
2547        KernelEffect::RequestApproval { requests } => requests
2548            .iter()
2549            .map(|request| request.call_id.clone())
2550            .collect(),
2551        KernelEffect::SpawnWorkflow { nodes, .. } => {
2552            nodes.iter().map(|node| node.agent_id.clone()).collect()
2553        }
2554        KernelEffect::PreemptSubAgents { agent_ids, .. } => agent_ids.clone(),
2555        KernelEffect::Done { .. } => Vec::new(),
2556        _ => vec![action.effect_id.clone()],
2557    }
2558}
2559
2560/// Map the ergonomic [`MemoryPolicy`] onto the flat `set_memory_policy` kernel event.
2561fn memory_policy_event(policy: MemoryPolicy) -> KernelInputEvent {
2562    KernelInputEvent::SetMemoryPolicy {
2563        memory_path: policy.memory_path,
2564        stale_warning_days: policy.stale_warning_days,
2565        retrieval_top_k: policy.retrieval_top_k,
2566        validation_enabled: policy.validation_enabled,
2567        max_content_bytes: policy.max_content_bytes,
2568        max_name_length: policy.max_name_length,
2569        promotion_recall_threshold: policy.promotion_recall_threshold,
2570    }
2571}
2572
2573fn next_archived_seq_start(events: Option<&[SessionEntry]>) -> u64 {
2574    let mut next = 0u64;
2575    for entry in events.unwrap_or_default() {
2576        if let SessionEvent::Compressed {
2577            archived_seq_range, ..
2578        } = &entry.event
2579        {
2580            next = next.max(archived_seq_range.1 + 1);
2581        }
2582    }
2583    next
2584}
2585
2586fn rendered_context_from_messages(
2587    messages: Vec<Message>,
2588) -> deepstrike_core::context::renderer::RenderedContext {
2589    let mut system_parts = Vec::new();
2590    let mut turns = Vec::new();
2591    for message in messages {
2592        if message.role == deepstrike_core::types::message::Role::System {
2593            if let Some(text) = message.content.as_text() {
2594                system_parts.push(text.to_owned());
2595            }
2596        } else {
2597            turns.push(message);
2598        }
2599    }
2600    let system_text = system_parts.join("\n\n");
2601    deepstrike_core::context::renderer::RenderedContext {
2602        system_text: system_text.clone(),
2603        system_stable: system_text,
2604        system_knowledge: String::new(),
2605        turns,
2606        state_turn: None,
2607        frozen_prefix_len: None,
2608        budget_overflow: None,
2609    }
2610}
2611
2612fn parse_update_plan_args(val: &serde_json::Value) -> TaskUpdate {
2613    let plan = val.get("plan").and_then(|v| {
2614        v.as_array().map(|arr| {
2615            arr.iter()
2616                .filter_map(|x| x.as_str().map(|s| s.to_string()))
2617                .collect()
2618        })
2619    });
2620    let current_step = val
2621        .get("current_step")
2622        .or_else(|| val.get("currentStep"))
2623        .and_then(|v| v.as_u64().map(|x| x as usize));
2624    let progress = val
2625        .get("progress")
2626        .and_then(|v| v.as_str().map(|s| s.to_string()));
2627    let scratchpad = val
2628        .get("scratchpad")
2629        .and_then(|v| v.as_str().map(|s| s.to_string()));
2630    let blocked_on = val
2631        .get("blocked_on")
2632        .or_else(|| val.get("blockedOn"))
2633        .and_then(|v| {
2634            v.as_array().map(|arr| {
2635                arr.iter()
2636                    .filter_map(|x| x.as_str().map(|s| s.to_string()))
2637                    .collect()
2638            })
2639        });
2640    let preserved_refs = val
2641        .get("preserved_refs")
2642        .or_else(|| val.get("preservedRefs"))
2643        .and_then(|v| {
2644            v.as_array().map(|arr| {
2645                arr.iter()
2646                    .filter_map(|x| x.as_str().map(|s| s.to_string()))
2647                    .collect()
2648            })
2649        });
2650    TaskUpdate {
2651        plan,
2652        current_step,
2653        progress,
2654        scratchpad,
2655        blocked_on,
2656        preserved_refs,
2657        // Directives are promoted in-kernel from acted-on signals; the SDK update path leaves them
2658        // untouched here (use `..` semantics) unless a future control plane curates them explicitly.
2659        directives: None,
2660    }
2661}