Skip to main content

deepstrike_sdk/runtime/
runner.rs

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