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            // P0-C: the skill loaded and in effect going into the current turn → per-turn metric.
837            let mut active_skill: Option<String> = None;
838            let session_start_ms = std::time::SystemTime::now()
839                .duration_since(std::time::UNIX_EPOCH)
840                .unwrap_or_default()
841                .as_millis() as u64;
842
843            let os_profile = assert_native_profile(self.opts.os_profile.clone())?;
844            let governance_policy = self
845                .opts
846                .governance_policy
847                .clone()
848                .unwrap_or(os_profile.governance_policy);
849            kernel_apply(
850                &mut kernel,
851                &mut pending_observations,
852                governance_policy.into_kernel_event(),
853            );
854
855            let attention_policy = self
856                .opts
857                .attention_policy
858                .unwrap_or(os_profile.attention_policy);
859            kernel_apply(
860                &mut kernel,
861                &mut pending_observations,
862                KernelInputEvent::SetAttentionPolicy {
863                    max_queue_size: attention_policy.max_queue_size.unwrap_or(64),
864                },
865            );
866
867            if let Some(max_wall_ms) = effective_wall_budget {
868                kernel_apply(
869                    &mut kernel,
870                    &mut pending_observations,
871                    KernelInputEvent::SetSchedulerBudget {
872                        max_wall_ms: Some(max_wall_ms),
873                    },
874                );
875            }
876
877            if let Some(quota) = self.opts.resource_quota.clone() {
878                kernel_apply(
879                    &mut kernel,
880                    &mut pending_observations,
881                    KernelInputEvent::SetResourceQuota { quota },
882                );
883            }
884
885            if let Some(policy) = self.opts.memory_policy.clone() {
886                kernel_apply(
887                    &mut kernel,
888                    &mut pending_observations,
889                    memory_policy_event(policy),
890                );
891            }
892
893            // I4: pre-fetch memory into the knowledge partition before the first LLM turn.
894            // Mirrors Node/WASM/Python preQueryMemory. Errs-open: missing dream_store/agent_id
895            // or a faulty closure silently skip the pre-fetch.
896            if !resume_mid_run {
897                if let (Some(pre), Some(store), Some(agent_id)) = (
898                    self.opts.pre_query_memory.clone(),
899                    self.opts.dream_store.as_ref(),
900                    self.opts.agent_id.as_deref(),
901                ) {
902                    let queries = pre(goal.as_str());
903                    let mut entries: Vec<deepstrike_core::mm::PageInEntry> = Vec::new();
904                    for q in &queries {
905                        let qt = q.trim();
906                        if qt.is_empty() {
907                            continue;
908                        }
909                        if let Ok(hits) = store.search(agent_id, qt, 5).await {
910                            for hit in hits {
911                                entries.push(deepstrike_core::mm::PageInEntry {
912                                    content: format!("[memory score={:.3}] {}", hit.score, hit.text),
913                                    tokens: None,
914                                    source: Some("memory".to_string()),
915                                });
916                            }
917                        }
918                    }
919                    if !entries.is_empty() {
920                        kernel_apply(
921                            &mut kernel,
922                            &mut pending_observations,
923                            KernelInputEvent::PageIn { entries },
924                        );
925                    }
926                }
927            }
928
929            let mut action = if resume_mid_run {
930                kernel_action(
931                    &mut kernel,
932                    &mut pending_observations,
933                    KernelInputEvent::Resume {
934                        approved_calls: vec![],
935                        denied_calls: vec![],
936                    },
937                )
938            } else {
939                // P0-A: fold an explicit `run_spec` and/or the `allowed_tool_ids` profile into the
940                // kernel's `capability_filter` (reuses the existing run_spec wire — no new ABI).
941                let run_spec = build_run_spec(
942                    self.opts.run_spec.clone(),
943                    self.opts.allowed_tool_ids.as_deref(),
944                    self.opts.agent_id.as_deref(),
945                    &session_id,
946                    &goal,
947                );
948                kernel_action(
949                    &mut kernel,
950                    &mut pending_observations,
951                    KernelInputEvent::StartRun {
952                        task: RuntimeTask::new(&goal).with_criteria(criteria),
953                        run_spec,
954                    },
955                )
956            };
957
958            let mut last_skill_version: u64 = skill_watcher.as_ref().map(|w| w.version()).unwrap_or(0);
959
960            while !kernel.lock().unwrap().is_terminal() {
961                if let KernelAction::ExecuteTool { .. } = &action {
962                    self.apply_kernel_page_in(
963                        &session_id,
964                        &kernel,
965                        &mut pending_observations,
966                    )
967                    .await;
968                }
969
970                // Hot-reload: refresh skill catalog if the watcher detected changes.
971                if let (Some(watcher), Some(skill_dir)) =
972                    (&skill_watcher, &self.opts.skill_dir)
973                {
974                    let cur = watcher.version();
975                    if cur != last_skill_version {
976                        last_skill_version = cur;
977                        kernel_apply(
978                            &mut kernel,
979                            &mut pending_observations,
980                            KernelInputEvent::SetAvailableSkills {
981                                skills: scan_skill_dir(skill_dir),
982                            },
983                        );
984                    }
985                }
986
987                next_archive_start = self
988                    .append_observations(
989                        &session_id,
990                        &kernel,
991                        &mut pending_observations,
992                        &mut pending_spool_outputs,
993                        next_archive_start,
994                    )
995                    .await;
996
997                if self.interrupted.load(Ordering::Relaxed) {
998                    kernel_apply(
999                        &mut kernel,
1000                        &mut pending_observations,
1001                        KernelInputEvent::Timeout,
1002                    );
1003                    break;
1004                }
1005
1006                if let Some(ss) = &self.opts.signal_source {
1007                    if let Some(sdk_sig) = ss.next_signal().await? {
1008                        let urgency = match sdk_sig.kind.as_str() {
1009                            "interrupt" => Urgency::Critical,
1010                            _ => Urgency::Normal,
1011                        };
1012                        let kernel_sig = KernelSignal::new(
1013                            KernelSignalSource::Custom,
1014                            KernelSignalType::Event,
1015                            urgency,
1016                            sdk_sig.kind.as_str(),
1017                        )
1018                        .with_payload(sdk_sig.payload.clone())
1019                        .with_timestamp(
1020                            std::time::SystemTime::now()
1021                                .duration_since(std::time::UNIX_EPOCH)
1022                                .unwrap_or_default()
1023                                .as_millis() as u64,
1024                        );
1025                        let executing = matches!(action, KernelAction::ExecuteTool { .. });
1026                        let disposition = router.ingest(kernel_sig, executing);
1027                        match disposition {
1028                            SignalDisposition::InterruptNow | SignalDisposition::Interrupt => {
1029                                // I0a: InterruptNow carries user_abort intent; see Node runner for rationale.
1030                                if matches!(disposition, SignalDisposition::InterruptNow) {
1031                                    self.interrupted.store(true, Ordering::Relaxed);
1032                                }
1033                                kernel_apply(
1034                                    &mut kernel,
1035                                    &mut pending_observations,
1036                                    KernelInputEvent::Timeout,
1037                                );
1038                                break;
1039                            }
1040                            _ => {}
1041                        }
1042                    }
1043                }
1044
1045                let mut queued = router.next();
1046                while let Some(sig) = queued {
1047                    if sig.urgency == Urgency::Critical {
1048                        kernel_apply(
1049                            &mut kernel,
1050                            &mut pending_observations,
1051                            KernelInputEvent::Timeout,
1052                        );
1053                        break;
1054                    }
1055                    queued = router.next();
1056                }
1057                if kernel.lock().unwrap().is_terminal() {
1058                    break;
1059                }
1060
1061                match &action {
1062                    KernelAction::CallProvider { context, tools } => {
1063                        let mut final_text = String::new();
1064                        let mut final_tool_calls: Vec<ToolCall> = Vec::new();
1065                        let mut turn_tokens: u32 = 0;
1066                        let mut turn_input_tokens: u32 = 0;
1067                        let mut turn_cache_read_tokens: u32 = 0;
1068                        let mut turn_cache_creation_tokens: u32 = 0;
1069                        let mut turn_cache_read_by_slot: Option<crate::providers::CacheReadBySlot> = None;
1070                        let mut turn_stop_reason: Option<String> = 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                                // Reactive recovery is now a kernel decision. Forward the raw
1107                                // provider error and dispatch whatever the kernel returns:
1108                                // CallProvider to retry with a freshly compacted context, or Done to
1109                                // terminate with an honest ContextOverflow. The classify + compact +
1110                                // retry + give-up policy lives in the kernel (one place), not
1111                                // duplicated across the four SDK runners.
1112                                let msg = e.to_string();
1113                                action = kernel_action(
1114                                    &mut kernel,
1115                                    &mut pending_observations,
1116                                    KernelInputEvent::ProviderError { message: msg.clone() },
1117                                );
1118                                // Withholding (query.ts parity): surface the raw provider error only
1119                                // when the kernel could NOT recover (it returned a terminal). On a
1120                                // recovered retry (CallProvider) the error stays hidden. `continue`
1121                                // re-enters the loop: a recovered turn persists its compaction
1122                                // archive at the loop's normal append point, and a terminal Done
1123                                // exits through `is_terminal()` into the run_terminal emit.
1124                                if matches!(action, KernelAction::Done { .. }) {
1125                                    yield RunEvent::Error(msg);
1126                                }
1127                                continue;
1128                            }
1129                        };
1130
1131                        while let Some(evt) = provider_stream.next().await {
1132                            match evt? {
1133                                StreamEvent::TextDelta { delta } => {
1134                                    final_text.push_str(&delta);
1135                                    yield RunEvent::TextDelta(delta);
1136                                }
1137                                StreamEvent::ThinkingDelta { delta } => {
1138                                    yield RunEvent::ThinkingDelta(delta);
1139                                }
1140                                StreamEvent::ToolCall { id, name, arguments } => {
1141                                    yield RunEvent::ToolCall { id: id.clone(), name: name.clone() };
1142                                    final_tool_calls.push(ToolCall {
1143                                        id: compact_str::CompactString::new(&id),
1144                                        name: compact_str::CompactString::new(&name),
1145                                        arguments,
1146                                    });
1147                                }
1148                                StreamEvent::Usage {
1149                                    total_tokens,
1150                                    input_tokens,
1151                                    cache_read_input_tokens,
1152                                    cache_creation_input_tokens,
1153                                    cache_read_input_tokens_by_slot,
1154                                    stop_reason,
1155                                    ..
1156                                } => {
1157                                    turn_tokens = total_tokens;
1158                                    // P0-C: capture input + prompt-cache split for the hit-rate baseline.
1159                                    turn_input_tokens = input_tokens;
1160                                    turn_cache_read_tokens = cache_read_input_tokens;
1161                                    turn_cache_creation_tokens = cache_creation_input_tokens;
1162                                    turn_cache_read_by_slot = cache_read_input_tokens_by_slot;
1163                                    // Phase 4: keep the last non-empty stop_reason for output-cap recovery.
1164                                    if stop_reason.is_some() { turn_stop_reason = stop_reason; }
1165                                }
1166                                StreamEvent::Done => {}
1167                            }
1168                        }
1169
1170                        let mut assistant = Message {
1171                            role: deepstrike_core::types::message::Role::Assistant,
1172                            content: deepstrike_core::types::message::Content::Text(final_text.clone()),
1173                            tool_calls: final_tool_calls.clone(),
1174                            token_count: if turn_tokens > 0 { Some(turn_tokens) } else { None },
1175                        };
1176
1177                        self.opts.provider.commit_stream_replay(&final_text, &final_tool_calls);
1178                        let mut provider_replay = peek_provider_replay(
1179                            self.opts.provider.as_ref(),
1180                            &final_text,
1181                            &final_tool_calls,
1182                        );
1183                        repair_llm_completed(&mut assistant, &mut provider_replay);
1184
1185                        action = kernel_action(
1186                            &mut kernel,
1187                            &mut pending_observations,
1188                            KernelInputEvent::ProviderResult {
1189                                message: assistant.clone(),
1190                                observed_input_tokens: None,
1191                                observed_output_tokens: None,
1192                                // COMPAT(gov-clock): rust SDK does not yet drive the in-kernel
1193                                // governance gate, so no clock is fed. Set once it adopts governancePolicy.
1194                                now_ms: None,
1195                                // Phase 4: stop_reason drives the kernel's max-output-tokens recovery.
1196                                stop_reason: turn_stop_reason.clone(),
1197                            },
1198                        );
1199                        self.log(
1200                            &session_id,
1201                            SessionEvent::LlmCompleted {
1202                                turn: kernel.lock().unwrap().state_machine().turn,
1203                                message: assistant,
1204                                provider_replay,
1205                            },
1206                        )
1207                        .await;
1208
1209                        // P0-C: per-turn tool-gating telemetry. `active_skill` reflects the skill in
1210                        // effect GOING INTO this turn; a `skill` call here only takes effect next turn
1211                        // — emit first, then advance.
1212                        if let Some(ref sink) = self.opts.on_turn_metrics {
1213                            sink(TurnMetrics {
1214                                turn: kernel.lock().unwrap().state_machine().turn,
1215                                tools_exposed,
1216                                tools_called: final_tool_calls.len(),
1217                                active_skill: active_skill.clone(),
1218                                input_tokens: turn_input_tokens,
1219                                cache_read_tokens: turn_cache_read_tokens,
1220                                cache_creation_tokens: turn_cache_creation_tokens,
1221                                cache_read_tokens_by_slot: turn_cache_read_by_slot.clone(),
1222                            });
1223                        }
1224                        if let Some(skill_call) =
1225                            final_tool_calls.iter().find(|c| c.name.as_str() == "skill")
1226                        {
1227                            if let Some(name) = skill_call.arguments.get("name").and_then(|v| v.as_str()) {
1228                                active_skill = Some(name.to_string());
1229                            }
1230                        }
1231                    }
1232                    KernelAction::ExecuteTool { calls } => {
1233                        let tool_calls = calls.clone();
1234                        self.log(
1235                            &session_id,
1236                            SessionEvent::ToolRequested {
1237                                turn: kernel.lock().unwrap().state_machine().turn,
1238                                calls: tool_calls.clone(),
1239                            },
1240                        )
1241                        .await;
1242
1243                        if let Some(gov) = &self.opts.governance {
1244                            let mut g = gov.lock().await;
1245                            if let Some(aid) = &self.opts.agent_id {
1246                                g.set_identity(aid, &session_id);
1247                            }
1248                        }
1249
1250                        let run_ctx = RunContext {
1251                            agent_id: self.opts.agent_id.as_deref(),
1252                            skill_dir: self.opts.skill_dir.as_deref(),
1253                            dream_store: self.opts.dream_store.as_deref(),
1254                            knowledge_source: self.opts.knowledge_source.as_deref(),
1255                            governance: self.opts.governance.clone(),
1256                            on_tool_suspend: self.opts.on_tool_suspend.clone(),
1257                            on_permission_request: self.opts.on_permission_request.clone(),
1258                        };
1259
1260                        let mut tool_results = Vec::new();
1261                        let mut normal_calls = Vec::new();
1262                        let mut plan_calls = Vec::new();
1263
1264                        for call in &tool_calls {
1265                            if call.name == "update_plan" {
1266                                plan_calls.push(call);
1267                            } else {
1268                                normal_calls.push(call.clone());
1269                            }
1270                        }
1271
1272                        for call in plan_calls {
1273                            let update = parse_update_plan_args(&call.arguments);
1274                            kernel_apply(
1275                                &mut kernel,
1276                                &mut pending_observations,
1277                                KernelInputEvent::UpdateTask { update },
1278                            );
1279                            tool_results.push(deepstrike_core::types::message::ToolResult {
1280                                call_id: call.id.clone(),
1281                                output: deepstrike_core::types::message::Content::Text("success".to_string()),
1282                                is_error: false,
1283                                is_fatal: false,
1284                                error_kind: None,
1285                                token_count: None,
1286                            });
1287                            yield RunEvent::ToolResult {
1288                                call_id: call.id.to_string(),
1289                                content: "success".to_string(),
1290                                is_error: false,
1291                                is_fatal: false,
1292                                error_kind: None,
1293                            };
1294                        }
1295
1296                        if !normal_calls.is_empty() {
1297                            let plane_stream = self.plane.execute_all(&normal_calls, run_ctx);
1298                            let mut stream = plane_stream;
1299                            while let Some(evt) = stream.next().await {
1300                                match evt? {
1301                                    RunEvent::ToolResult {
1302                                        call_id,
1303                                        content,
1304                                        is_error,
1305                                        is_fatal,
1306                                        error_kind,
1307                                    } => {
1308                                        tool_results.push(deepstrike_core::types::message::ToolResult {
1309                                            call_id: compact_str::CompactString::new(&call_id),
1310                                            output: deepstrike_core::types::message::Content::Text(content),
1311                                            is_error,
1312                                            is_fatal,
1313                                            error_kind,
1314                                            token_count: None,
1315                                        });
1316                                    }
1317                                    RunEvent::ToolArgumentRepaired { call_id, name, original_arguments, repaired_arguments } => {
1318                                        self.log(
1319                                            &session_id,
1320                                            SessionEvent::ToolArgumentRepaired {
1321                                                turn: kernel.lock().unwrap().state_machine().turn,
1322                                                tool: name.clone(),
1323                                                original_arguments: original_arguments.clone(),
1324                                                repaired_arguments: repaired_arguments.clone(),
1325                                            },
1326                                        )
1327                                        .await;
1328                                        yield RunEvent::ToolArgumentRepaired {
1329                                            call_id,
1330                                            name,
1331                                            original_arguments,
1332                                            repaired_arguments,
1333                                        };
1334                                    }
1335                                    RunEvent::ToolDenied { call_id, tool_name, reason } => {
1336                                        self.log(
1337                                            &session_id,
1338                                            SessionEvent::ToolDenied {
1339                                                turn: kernel.lock().unwrap().state_machine().turn,
1340                                                call_id: call_id.clone(),
1341                                                tool_name: tool_name.clone(),
1342                                                reason: reason.clone(),
1343                                            },
1344                                        )
1345                                        .await;
1346                                        yield RunEvent::ToolDenied { call_id, tool_name, reason };
1347                                    }
1348                                    RunEvent::PermissionRequest { call_id, tool_name, arguments, reason } => {
1349                                        let turn = kernel.lock().unwrap().state_machine().turn;
1350                                        self.log(
1351                                            &session_id,
1352                                            SessionEvent::PermissionRequested {
1353                                                turn,
1354                                                tool: tool_name.clone(),
1355                                                arguments: arguments.clone(),
1356                                                reason: Some(reason.clone()),
1357                                            },
1358                                        )
1359                                        .await;
1360                                        yield RunEvent::PermissionRequest { call_id, tool_name, arguments, reason };
1361                                    }
1362                                    RunEvent::PermissionResolved { call_id, tool_name, approved, responder, reason } => {
1363                                        let turn = kernel.lock().unwrap().state_machine().turn;
1364                                        self.log(
1365                                            &session_id,
1366                                            SessionEvent::PermissionResolved {
1367                                                turn,
1368                                                approved,
1369                                                responder: responder.clone(),
1370                                            },
1371                                        )
1372                                        .await;
1373                                        yield RunEvent::PermissionResolved { call_id, tool_name, approved, responder, reason };
1374                                    }
1375                                    other => yield other,
1376                                }
1377                            }
1378                            let names: Vec<String> = normal_calls.iter().map(|c| c.name.to_string()).collect();
1379                            kernel_apply(
1380                                &mut kernel,
1381                                &mut pending_observations,
1382                                KernelInputEvent::UpdateTask {
1383                                    update: TaskUpdate {
1384                                        progress: Some(format!("Executed tools: {}", names.join(", "))),
1385                                        ..Default::default()
1386                                    },
1387                                },
1388                            );
1389                        }
1390
1391                        self.log(
1392                            &session_id,
1393                            SessionEvent::ToolCompleted {
1394                                turn: kernel.lock().unwrap().state_machine().turn,
1395                                results: tool_results.clone(),
1396                            },
1397                        )
1398                        .await;
1399
1400                        for call in &normal_calls {
1401                            if let Some(result) = tool_results
1402                                .iter()
1403                                .find(|r| r.call_id.as_str() == call.id.as_str())
1404                            {
1405                                let output = match &result.output {
1406                                    deepstrike_core::types::message::Content::Text(s) => s.to_string(),
1407                                    deepstrike_core::types::message::Content::Parts(parts) => {
1408                                        serde_json::to_string(parts).unwrap_or_default()
1409                                    }
1410                                };
1411                                pending_spool_outputs.insert(
1412                                    call.id.to_string(),
1413                                    (call.name.to_string(), output),
1414                                );
1415                            }
1416                        }
1417
1418                        // P1-B B3: a successfully-resolved `skill` call activates that skill for the
1419                        // next turn (fed before ToolResults, which computes the next action).
1420                        for call in &tool_calls {
1421                            if call.name.as_str() != "skill" {
1422                                continue;
1423                            }
1424                            let ok = tool_results
1425                                .iter()
1426                                .any(|r| r.call_id.as_str() == call.id.as_str() && !r.is_error);
1427                            if !ok {
1428                                continue;
1429                            }
1430                            if let Some(name) = call.arguments.get("name").and_then(|v| v.as_str()) {
1431                                kernel_apply(
1432                                    &mut kernel,
1433                                    &mut pending_observations,
1434                                    KernelInputEvent::SkillActivated { name: name.to_string() },
1435                                );
1436                            }
1437                        }
1438
1439                        action = kernel_action(
1440                            &mut kernel,
1441                            &mut pending_observations,
1442                            KernelInputEvent::ToolResults {
1443                                results: tool_results,
1444                            },
1445                        );
1446                    }
1447                    KernelAction::EvaluateMilestone {
1448                        phase_id,
1449                        criteria,
1450                        required_evidence,
1451                        ..
1452                    } => {
1453                        let policy = self.opts.milestone_policy;
1454                        if policy == MilestonePolicy::AutoPass {
1455                            let result = MilestoneCheckResult::pass(phase_id.clone());
1456                            action = kernel_action(
1457                                &mut kernel,
1458                                &mut pending_observations,
1459                                KernelInputEvent::MilestoneResult { result },
1460                            );
1461                            next_archive_start = self
1462                                .append_observations(
1463                                    &session_id,
1464                                    &kernel,
1465                                    &mut pending_observations,
1466                                    &mut pending_spool_outputs,
1467                                    next_archive_start,
1468                                )
1469                                .await;
1470                        } else if let Some(handler) = &self.opts.on_milestone_evaluate {
1471                            let context = MilestoneEvaluationContext {
1472                                phase_id: phase_id.clone(),
1473                                criteria: criteria.clone(),
1474                                required_evidence: required_evidence.clone(),
1475                            };
1476                            let check_future = handler(context);
1477                            let result = check_future.await?;
1478                            action = kernel_action(
1479                                &mut kernel,
1480                                &mut pending_observations,
1481                                KernelInputEvent::MilestoneResult { result },
1482                            );
1483                            next_archive_start = self
1484                                .append_observations(
1485                                    &session_id,
1486                                    &kernel,
1487                                    &mut pending_observations,
1488                                    &mut pending_spool_outputs,
1489                                    next_archive_start,
1490                                )
1491                                .await;
1492                        } else {
1493                            next_archive_start = self
1494                                .append_observations(
1495                                    &session_id,
1496                                    &kernel,
1497                                    &mut pending_observations,
1498                                    &mut pending_spool_outputs,
1499                                    next_archive_start,
1500                                )
1501                                .await;
1502                            self.log(
1503                                &session_id,
1504                                SessionEvent::RunTerminal {
1505                                    reason: "milestone_pending".to_string(),
1506                                    turns_used: kernel.lock().unwrap().state_machine().turn.max(1),
1507                                    total_tokens: 0,
1508                                },
1509                            )
1510                            .await;
1511                            yield RunEvent::Done {
1512                                iterations: kernel.lock().unwrap().state_machine().turn.max(1),
1513                                total_tokens: 0,
1514                                status: "milestone_pending".to_string(),
1515                            };
1516                            return;
1517                        }
1518                    }
1519                    KernelAction::Done { result } => {
1520                        let status = format!("{:?}", result.termination).to_lowercase();
1521                        let turns_used = result.turns_used.max(1);
1522                        let total_tokens = result.total_tokens_used;
1523
1524                        next_archive_start = self
1525                            .append_observations(
1526                                &session_id,
1527                                &kernel,
1528                                &mut pending_observations,
1529                                &mut pending_spool_outputs,
1530                                next_archive_start,
1531                            )
1532                            .await;
1533
1534                        self.log(
1535                            &session_id,
1536                            SessionEvent::RunTerminal {
1537                                reason: status.clone(),
1538                                turns_used,
1539                                total_tokens,
1540                            },
1541                        )
1542                        .await;
1543
1544                        if let (Some(store), Some(agent_id)) =
1545                            (&self.opts.dream_store, &self.opts.agent_id)
1546                        {
1547                            let new_msgs = kernel.lock().unwrap().state_machine_mut().drain_new_messages();
1548                            if !new_msgs.is_empty() {
1549                                let now_ms = std::time::SystemTime::now()
1550                                    .duration_since(std::time::UNIX_EPOCH)
1551                                    .unwrap_or_default()
1552                                    .as_millis() as u64;
1553                                let session = deepstrike_core::memory::durable::SessionData {
1554                                    session_id: uuid::Uuid::new_v4().to_string(),
1555                                    agent_id: agent_id.clone(),
1556                                    messages: new_msgs,
1557                                    metadata: serde_json::Value::Null,
1558                                    created_at_ms: session_start_ms,
1559                                    updated_at_ms: now_ms,
1560                                };
1561                                let _ = store.save_session(session).await;
1562                            }
1563                        }
1564
1565                        yield RunEvent::Done {
1566                            iterations: turns_used,
1567                            total_tokens,
1568                            status,
1569                        };
1570                        return;
1571                    }
1572                }
1573            }
1574
1575            next_archive_start = self
1576                .append_observations(
1577                    &session_id,
1578                    &kernel,
1579                    &mut pending_observations,
1580                    &mut pending_spool_outputs,
1581                    next_archive_start,
1582                )
1583                .await;
1584
1585            // I0a: when the loop exits without a clean kernel-done, preserve preempt intent
1586            // (interrupted flag set) in the run_terminal reason — otherwise an interrupt-curtailed
1587            // run reports "error" indistinguishable from a real crash. Mirrors Node/WASM/Python.
1588            let (status, turns_used, total_tokens) = match &action {
1589                KernelAction::Done { result } => (
1590                    format!("{:?}", result.termination).to_lowercase(),
1591                    result.turns_used.max(1),
1592                    result.total_tokens_used,
1593                ),
1594                _ => (
1595                    if self.interrupted.load(Ordering::Relaxed) { "user_abort".to_string() } else { "error".to_string() },
1596                    kernel.lock().unwrap().state_machine().turn.max(1),
1597                    0,
1598                ),
1599            };
1600
1601            self.log(
1602                &session_id,
1603                SessionEvent::RunTerminal {
1604                    reason: status.clone(),
1605                    turns_used,
1606                    total_tokens,
1607                },
1608            )
1609            .await;
1610
1611            if let KernelAction::Done { .. } = &action {
1612                if let (Some(store), Some(agent_id)) =
1613                    (&self.opts.dream_store, &self.opts.agent_id)
1614                {
1615                    let new_msgs = kernel.lock().unwrap().state_machine_mut().drain_new_messages();
1616                    if !new_msgs.is_empty() {
1617                        let now_ms = std::time::SystemTime::now()
1618                            .duration_since(std::time::UNIX_EPOCH)
1619                            .unwrap_or_default()
1620                            .as_millis() as u64;
1621                        let session = deepstrike_core::memory::durable::SessionData {
1622                            session_id: uuid::Uuid::new_v4().to_string(),
1623                            agent_id: agent_id.clone(),
1624                            messages: new_msgs,
1625                            metadata: serde_json::Value::Null,
1626                            created_at_ms: session_start_ms,
1627                            updated_at_ms: now_ms,
1628                        };
1629                        let _ = store.save_session(session).await;
1630                    }
1631                }
1632            }
1633
1634            yield RunEvent::Done {
1635                iterations: turns_used,
1636                total_tokens,
1637                status,
1638            };
1639        }
1640    }
1641
1642    pub(crate) async fn append_observations(
1643        &self,
1644        session_id: &str,
1645        kernel_mutex: &std::sync::Mutex<KernelRuntime>,
1646        observations: &mut Vec<KernelObservation>,
1647        pending_spool_outputs: &mut std::collections::HashMap<String, (String, String)>,
1648        mut next_archive_start: u64,
1649    ) -> u64 {
1650        let drained = std::mem::take(observations);
1651        let (turn, preserved_refs, summary_tokens_by_index) = {
1652            let kernel = kernel_mutex.lock().unwrap();
1653            let sm = kernel.state_machine();
1654            let summary_tokens_by_index = drained
1655                .iter()
1656                .map(|obs| match obs {
1657                    KernelObservation::Compressed { summary, .. } => {
1658                        summary.as_ref().map(|s| sm.ctx.engine.count(s))
1659                    }
1660                    _ => None,
1661                })
1662                .collect::<Vec<_>>();
1663            (
1664                sm.turn,
1665                sm.ctx.partitions.task_state.preserved_refs.clone(),
1666                summary_tokens_by_index,
1667            )
1668        };
1669
1670        for (index, obs) in drained.into_iter().enumerate() {
1671            match obs {
1672                KernelObservation::Compressed {
1673                    action,
1674                    rho_after: _,
1675                    summary,
1676                    archived,
1677                    invalidates_prefix_at: _,
1678                } => {
1679                    let Some(log) = &self.opts.session_log else {
1680                        continue;
1681                    };
1682                    let latest = log.latest_seq(session_id).await.unwrap_or(-1) as u64;
1683                    if latest < next_archive_start {
1684                        continue;
1685                    }
1686                    let end = latest;
1687
1688                    let mut archive_ref = None;
1689                    if let Some(store) = &self.opts.compression_store {
1690                        if !archived.is_empty() {
1691                            if let Ok(path_ref) =
1692                                store.write(session_id, next_archive_start, &archived)
1693                            {
1694                                if !path_ref.is_empty() {
1695                                    archive_ref = Some(path_ref);
1696                                }
1697                            }
1698                        }
1699                    }
1700
1701                    let summary_tokens = summary_tokens_by_index.get(index).copied().flatten();
1702                    let action_str = match action {
1703                        KernelPressureAction::None => "none".to_string(),
1704                        KernelPressureAction::SnipCompact => "snip_compact".to_string(),
1705                        KernelPressureAction::MicroCompact => "micro_compact".to_string(),
1706                        KernelPressureAction::ContextCollapse => "context_collapse".to_string(),
1707                        KernelPressureAction::AutoCompact => "auto_compact".to_string(),
1708                    };
1709
1710                    if let Ok(compressed_seq) = log
1711                        .append(
1712                            session_id,
1713                            SessionEvent::Compressed {
1714                                turn,
1715                                archived_seq_range: (next_archive_start, end),
1716                                category: Some(category_for_kind("compressed")),
1717                                primitive: Some(primitive_for_kind("compressed")),
1718                                action: Some(action_str),
1719                                summary: summary.clone(),
1720                                summary_tokens,
1721                                archive_ref,
1722                                preserved_refs: preserved_refs.clone(),
1723                            },
1724                        )
1725                        .await
1726                    {
1727                        next_archive_start = compressed_seq + 1;
1728                    }
1729                }
1730                KernelObservation::Rollbacked {
1731                    turn,
1732                    checkpoint_history_len,
1733                    reason,
1734                } => {
1735                    self.log(
1736                        session_id,
1737                        SessionEvent::Rollbacked {
1738                            turn,
1739                            category: Some(category_for_kind("rollbacked")),
1740                            primitive: Some(primitive_for_kind("rollbacked")),
1741                            checkpoint_history_len,
1742                            reason,
1743                        },
1744                    )
1745                    .await;
1746                }
1747                KernelObservation::CapabilityChanged {
1748                    turn,
1749                    added,
1750                    removed,
1751                    change_kind,
1752                    capability_id,
1753                    version,
1754                    mounted_by,
1755                    mount_reason,
1756                } => {
1757                    self.log(
1758                        session_id,
1759                        SessionEvent::CapabilityChanged {
1760                            turn,
1761                            category: Some(category_for_kind("capability_changed")),
1762                            primitive: Some(primitive_for_kind("capability_changed")),
1763                            added,
1764                            removed,
1765                            change_kind,
1766                            capability_id,
1767                            version,
1768                            mounted_by,
1769                            mount_reason,
1770                        },
1771                    )
1772                    .await;
1773                }
1774                KernelObservation::MilestoneAdvanced {
1775                    turn,
1776                    phase_id,
1777                    capabilities_unlocked,
1778                } => {
1779                    self.log(
1780                        session_id,
1781                        SessionEvent::MilestoneAdvanced {
1782                            turn,
1783                            category: Some(category_for_kind("milestone_advanced")),
1784                            primitive: Some(primitive_for_kind("milestone_advanced")),
1785                            phase_id,
1786                            capabilities_unlocked,
1787                        },
1788                    )
1789                    .await;
1790                }
1791                KernelObservation::MilestoneBlocked {
1792                    turn,
1793                    phase_id,
1794                    reason,
1795                } => {
1796                    self.log(
1797                        session_id,
1798                        SessionEvent::MilestoneBlocked {
1799                            turn,
1800                            category: Some(category_for_kind("milestone_blocked")),
1801                            primitive: Some(primitive_for_kind("milestone_blocked")),
1802                            phase_id,
1803                            reason,
1804                        },
1805                    )
1806                    .await;
1807                }
1808                KernelObservation::MilestoneEvidence {
1809                    turn,
1810                    phase_id,
1811                    evidence,
1812                } => {
1813                    self.log(
1814                        session_id,
1815                        SessionEvent::MilestoneEvidence {
1816                            turn,
1817                            category: Some(category_for_kind("milestone_evidence")),
1818                            primitive: Some(primitive_for_kind("milestone_evidence")),
1819                            phase_id,
1820                            evidence,
1821                        },
1822                    )
1823                    .await;
1824                }
1825                KernelObservation::Renewed { .. } => {}
1826                KernelObservation::CheckpointTaken { turn, history_len } => {
1827                    self.log(
1828                        session_id,
1829                        SessionEvent::CheckpointTaken {
1830                            turn,
1831                            category: Some(category_for_kind("checkpoint_taken")),
1832                            primitive: Some(primitive_for_kind("checkpoint_taken")),
1833                            history_len,
1834                        },
1835                    )
1836                    .await;
1837                }
1838                KernelObservation::AgentProcessChanged { .. } => {}
1839                // W0-ABI workflow lifecycle. The rust SDK has no workflow drive yet
1840                // (node/python only), so these are observed-but-ignored here.
1841                KernelObservation::WorkflowBatchSpawned { .. } => {}
1842                KernelObservation::WorkflowCompleted { .. } => {}
1843                KernelObservation::AgentPreempted { .. } => {}
1844                // Governance flagged a tool call for user approval. The kernel does
1845                // not block it; the SDK-side human-approval workflow is a follow-up.
1846                KernelObservation::ToolGated { .. } => {}
1847                // In-kernel signal routing decision. The rust SDK does not yet drive
1848                // signals through the kernel attention policy; observation is logged
1849                // by the generic observation path elsewhere if needed.
1850                KernelObservation::SignalDisposed { .. } => {}
1851                KernelObservation::BudgetExceeded { .. } => {}
1852                KernelObservation::Suspended { .. } => {}
1853                KernelObservation::Resumed { .. } => {}
1854                KernelObservation::PageOut {
1855                    turn,
1856                    action,
1857                    rho_after: _,
1858                    summary,
1859                    archived,
1860                    tier_hint,
1861                } => {
1862                    if !archived.is_empty() {
1863                        self.local_page_out_cache
1864                            .lock()
1865                            .unwrap()
1866                            .extend(archived.clone());
1867                    }
1868
1869                    let action_str = match action {
1870                        KernelPressureAction::None => "none".to_string(),
1871                        KernelPressureAction::SnipCompact => "snip_compact".to_string(),
1872                        KernelPressureAction::MicroCompact => "micro_compact".to_string(),
1873                        KernelPressureAction::ContextCollapse => "context_collapse".to_string(),
1874                        KernelPressureAction::AutoCompact => "auto_compact".to_string(),
1875                    };
1876
1877                    self.log(
1878                        session_id,
1879                        SessionEvent::PageOut {
1880                            turn,
1881                            category: Some(category_for_kind("page_out")),
1882                            primitive: Some(primitive_for_kind("page_out")),
1883                            action: Some(action_str.clone()),
1884                            summary: summary.clone(),
1885                            tier_hint: Some(tier_hint.clone()),
1886                            message_count: archived.len() as u32,
1887                        },
1888                    )
1889                    .await;
1890
1891                    if tier_hint == "semantic" && !archived.is_empty() {
1892                        self.archive_semantic_page_out(archived, Some(action_str))
1893                            .await;
1894                    }
1895                }
1896                KernelObservation::PageInRequested { .. } => {}
1897                KernelObservation::MemoryWritten {
1898                    turn,
1899                    memory_id,
1900                    memory_kind,
1901                    size_bytes,
1902                } => {
1903                    self.log(
1904                        session_id,
1905                        SessionEvent::MemoryWritten {
1906                            turn,
1907                            category: Some(category_for_kind("memory_written")),
1908                            primitive: Some(primitive_for_kind("memory_written")),
1909                            memory_id,
1910                            memory_kind,
1911                            size_bytes,
1912                        },
1913                    )
1914                    .await;
1915                }
1916                KernelObservation::MemoryQueried {
1917                    turn,
1918                    query_context,
1919                    requested_k,
1920                    requires_async_response,
1921                } => {
1922                    self.log(
1923                        session_id,
1924                        SessionEvent::MemoryQueried {
1925                            turn,
1926                            category: Some(category_for_kind("memory_queried")),
1927                            primitive: Some(primitive_for_kind("memory_queried")),
1928                            query_context,
1929                            requested_k,
1930                            requires_async_response,
1931                        },
1932                    )
1933                    .await;
1934                }
1935                // Phase 7 / M3: no dedicated session kinds yet in rust SDK.
1936                KernelObservation::MemoryValidationFailed {
1937                    turn,
1938                    memory_id,
1939                    error,
1940                } => {
1941                    self.log(
1942                        session_id,
1943                        SessionEvent::MemoryValidationFailed {
1944                            turn,
1945                            category: Some(category_for_kind("memory_validation_failed")),
1946                            primitive: Some(primitive_for_kind("memory_validation_failed")),
1947                            memory_id,
1948                            error,
1949                        },
1950                    )
1951                    .await;
1952                }
1953                KernelObservation::LargeResultSpooled {
1954                    turn,
1955                    call_id,
1956                    tool,
1957                    original_size,
1958                    preview_size,
1959                    spool_ref: _,
1960                } => {
1961                    let mut spool_ref = None;
1962                    let mut tool_name = tool;
1963                    if let Some((stored_tool, output)) = pending_spool_outputs.remove(&call_id) {
1964                        if tool_name.is_empty() {
1965                            tool_name = stored_tool;
1966                        }
1967                        if let Ok(path) =
1968                            crate::runtime::large_result_spool::persist_output(&call_id, &output)
1969                        {
1970                            spool_ref = Some(path);
1971                        }
1972                    }
1973                    self.log(
1974                        session_id,
1975                        SessionEvent::LargeResultSpooled {
1976                            turn,
1977                            category: Some(category_for_kind("large_result_spooled")),
1978                            primitive: Some(primitive_for_kind("large_result_spooled")),
1979                            call_id,
1980                            tool: tool_name,
1981                            original_size,
1982                            preview_size,
1983                            spool_ref,
1984                        },
1985                    )
1986                    .await;
1987                }
1988            }
1989        }
1990        next_archive_start
1991    }
1992
1993    async fn read_entries(&self, session_id: &str) -> Result<Vec<SessionEntry>> {
1994        if let Some(log) = &self.opts.session_log {
1995            log.read(session_id, 0, None).await.map_err(Error::Io)
1996        } else {
1997            Ok(Vec::new())
1998        }
1999    }
2000
2001    async fn log(&self, session_id: &str, event: SessionEvent) {
2002        if let Some(log) = &self.opts.session_log {
2003            let _ = log.append(session_id, event).await;
2004        }
2005    }
2006
2007    async fn apply_kernel_page_in(
2008        &self,
2009        session_id: &str,
2010        kernel_mutex: &std::sync::Mutex<KernelRuntime>,
2011        observations: &mut Vec<KernelObservation>,
2012    ) {
2013        let requests: Vec<_> = observations
2014            .iter()
2015            .filter_map(|obs| match obs {
2016                KernelObservation::PageInRequested {
2017                    turn: _,
2018                    call_id,
2019                    tool,
2020                    query,
2021                    top_k,
2022                } => Some((call_id.clone(), tool.clone(), query.clone(), *top_k)),
2023                _ => None,
2024            })
2025            .collect();
2026
2027        if requests.is_empty() {
2028            return;
2029        }
2030
2031        let mut entries = Vec::new();
2032        for (_call_id, tool, query, top_k) in requests {
2033            let top_k = top_k as usize;
2034            if tool == "memory" {
2035                // Priority 1: Local Page-Out Cache (keyword matching)
2036                let local_hits = {
2037                    let cache = self.local_page_out_cache.lock().unwrap();
2038                    cache
2039                        .iter()
2040                        .filter(|m| {
2041                            let content_str = message_content_as_text(&m.content);
2042                            content_str.to_lowercase().contains(&query.to_lowercase())
2043                        })
2044                        .cloned()
2045                        .take(top_k)
2046                        .collect::<Vec<_>>()
2047                };
2048
2049                for hit in &local_hits {
2050                    let role_str = match hit.role {
2051                        deepstrike_core::types::message::Role::System => "system",
2052                        deepstrike_core::types::message::Role::User => "user",
2053                        deepstrike_core::types::message::Role::Assistant => "assistant",
2054                        deepstrike_core::types::message::Role::Tool => "tool",
2055                    };
2056                    let content_str = message_content_as_text(&hit.content);
2057                    entries.push(deepstrike_core::mm::PageInEntry {
2058                        content: format!("[local semantic cache] {}: {}", role_str, content_str),
2059                        tokens: None,
2060                        source: Some("semantic_cache".to_string()),
2061                    });
2062                }
2063
2064                let remaining_k = top_k.saturating_sub(local_hits.len());
2065                if remaining_k > 0 {
2066                    if let (Some(store), Some(agent_id)) = (&self.opts.dream_store, &self.opts.agent_id) {
2067                        if let Ok(hits) = store.search(agent_id, &query, remaining_k).await {
2068                            for hit in hits {
2069                                entries.push(deepstrike_core::mm::PageInEntry {
2070                                    content: format!("[memory score={:.3}] {}", hit.score, hit.text),
2071                                    tokens: None,
2072                                    source: Some("memory".to_string()),
2073                                });
2074                            }
2075                        }
2076                    }
2077                }
2078            } else if tool == "knowledge" {
2079                if let Some(source) = &self.opts.knowledge_source {
2080                    if let Ok(snippets) = source.retrieve(&query, top_k).await {
2081                        for snippet in snippets {
2082                            entries.push(deepstrike_core::mm::PageInEntry {
2083                                content: snippet,
2084                                tokens: None,
2085                                source: Some("knowledge".to_string()),
2086                            });
2087                        }
2088                    }
2089                }
2090            }
2091        }
2092
2093        if entries.is_empty() {
2094            return;
2095        }
2096
2097        // Apply back to the kernel
2098        let mut kernel = kernel_mutex.lock().unwrap();
2099        let turn = kernel.state_machine().turn;
2100        let step = kernel.step(KernelInput::new(KernelInputEvent::PageIn {
2101            entries: entries.clone(),
2102        }));
2103        observations.extend(step.observations);
2104
2105        // Append PageIn event to session log
2106        self.log(
2107            session_id,
2108            SessionEvent::PageIn {
2109                turn,
2110                category: Some(category_for_kind("page_in")),
2111                primitive: Some(primitive_for_kind("page_in")),
2112                entry_count: entries.len() as u32,
2113            },
2114        )
2115        .await;
2116    }
2117
2118    async fn archive_semantic_page_out(&self, archived: Vec<Message>, action: Option<String>) {
2119        let (Some(store), Some(agent_id)) = (&self.opts.dream_store, &self.opts.agent_id) else {
2120            return;
2121        };
2122
2123        let summary = match self.summarize_for_long_term_memory(&archived).await {
2124            Ok(s) => s,
2125            Err(_) => return, // non-fatal
2126        };
2127
2128        if let Ok(existing) = store.load_memories(agent_id).await {
2129            let curation_result = deepstrike_core::memory::curator::CurationResult {
2130                to_add: vec![deepstrike_core::memory::semantic::MemoryEntry {
2131                    text: summary,
2132                    score: 1.0,
2133                    metadata: serde_json::json!({
2134                        "source": "semantic_page_out",
2135                        "action": action,
2136                    }),
2137                }],
2138                to_remove_indices: vec![],
2139                stats: deepstrike_core::memory::curator::CurationStats {
2140                    insights_processed: 1,
2141                    duplicates_removed: 0,
2142                    conflicts_resolved: 0,
2143                    entries_added: 1,
2144                },
2145            };
2146            let _ = store.commit(agent_id, curation_result, &existing).await;
2147        }
2148    }
2149
2150    async fn summarize_for_long_term_memory(&self, archived: &[Message]) -> crate::Result<String> {
2151        let transcript = archived
2152            .iter()
2153            .map(|m| {
2154                let role_str = match m.role {
2155                    deepstrike_core::types::message::Role::System => "system",
2156                    deepstrike_core::types::message::Role::User => "user",
2157                    deepstrike_core::types::message::Role::Assistant => "assistant",
2158                    deepstrike_core::types::message::Role::Tool => "tool",
2159                };
2160                let content_str = message_content_as_text(&m.content);
2161                format!("{}: {}", role_str, content_str)
2162            })
2163            .collect::<Vec<_>>()
2164            .join("\n");
2165
2166        let system_prompt_opt = self.opts.system_prompt.as_deref();
2167        let system_text = match system_prompt_opt {
2168            Some(sp) => format!(
2169                "{}\n\nSummarize the following conversation for long-term memory. Preserve key facts, decisions, and open questions.",
2170                sp
2171            ),
2172            None => "Summarize the following conversation for long-term memory. Preserve key facts, decisions, and open questions.".to_string(),
2173        };
2174
2175        let context = deepstrike_core::context::renderer::RenderedContext {
2176            system_text,
2177            system_stable: String::new(),
2178            system_knowledge: String::new(),
2179            turns: vec![deepstrike_core::types::message::Message {
2180                role: deepstrike_core::types::message::Role::User,
2181                content: deepstrike_core::types::message::Content::Text(transcript.clone()),
2182                tool_calls: vec![],
2183                token_count: None,
2184            }],
2185            state_turn: None,
2186            frozen_prefix_len: None,
2187        };
2188
2189        let synth_state = self.opts.provider.create_run_state();
2190        let mut stream = self
2191            .opts
2192            .provider
2193            .stream(&context, &[], None, synth_state.as_ref())
2194            .await?;
2195
2196        let mut synthesis_text = String::new();
2197        while let Some(evt) = stream.next().await {
2198            if let Ok(StreamEvent::TextDelta { delta }) = evt {
2199                synthesis_text.push_str(&delta);
2200            }
2201        }
2202
2203        let text = synthesis_text.trim();
2204        if text.is_empty() {
2205            Ok(transcript.chars().take(2000).collect())
2206        } else {
2207            Ok(text.to_string())
2208        }
2209    }
2210}
2211
2212fn message_content_as_text(content: &deepstrike_core::types::message::Content) -> String {
2213    match content {
2214        deepstrike_core::types::message::Content::Text(s) => s.clone(),
2215        deepstrike_core::types::message::Content::Parts(parts) => parts
2216            .iter()
2217            .filter_map(|p| match p {
2218                deepstrike_core::types::message::ContentPart::Text { text } => Some(text.as_str()),
2219                deepstrike_core::types::message::ContentPart::ToolResult { output, .. } => Some(output.as_str()),
2220                _ => None,
2221            })
2222            .collect::<Vec<_>>()
2223            .join("\n"),
2224    }
2225}
2226
2227fn kernel_apply(
2228    kernel: &mut KernelRuntime,
2229    pending_observations: &mut Vec<KernelObservation>,
2230    event: KernelInputEvent,
2231) {
2232    let step = kernel.step(KernelInput::new(event));
2233    pending_observations.extend(step.observations);
2234}
2235
2236fn kernel_action(
2237    kernel: &mut KernelRuntime,
2238    pending_observations: &mut Vec<KernelObservation>,
2239    event: KernelInputEvent,
2240) -> KernelAction {
2241    let mut step = kernel.step(KernelInput::new(event));
2242    pending_observations.append(&mut step.observations);
2243    take_single_action(step)
2244}
2245
2246fn take_single_action(mut step: KernelStep) -> KernelAction {
2247    step.actions
2248        .pop()
2249        .expect("kernel transition must return one action")
2250}
2251
2252pub async fn collect_text(
2253    mut stream: std::pin::Pin<Box<dyn futures::Stream<Item = Result<RunEvent>> + '_>>,
2254) -> Result<String> {
2255    let mut text = String::new();
2256    while let Some(evt) = stream.next().await {
2257        if let RunEvent::TextDelta(d) = evt? {
2258            text.push_str(&d);
2259        }
2260    }
2261    Ok(text)
2262}
2263
2264fn merge_extensions(
2265    base: Option<&serde_json::Value>,
2266    over: Option<&serde_json::Value>,
2267) -> Option<serde_json::Value> {
2268    match (base, over) {
2269        (Some(b), Some(o)) => {
2270            let mut merged = b.clone();
2271            if let (Some(m), Some(obj)) = (merged.as_object_mut(), o.as_object()) {
2272                for (k, v) in obj {
2273                    m.insert(k.clone(), v.clone());
2274                }
2275            }
2276            Some(merged)
2277        }
2278        (Some(b), None) => Some(b.clone()),
2279        (None, Some(o)) => Some(o.clone()),
2280        (None, None) => None,
2281    }
2282}
2283
2284fn effective_wall_budget(
2285    scheduler_budget: Option<SchedulerBudget>,
2286    fallback_timeout_ms: Option<u64>,
2287) -> Option<u64> {
2288    scheduler_budget
2289        .and_then(|budget| budget.max_wall_ms)
2290        .or(fallback_timeout_ms)
2291}
2292
2293/// Map the ergonomic [`MemoryPolicy`] onto the flat `set_memory_policy` kernel event.
2294fn memory_policy_event(policy: MemoryPolicy) -> KernelInputEvent {
2295    KernelInputEvent::SetMemoryPolicy {
2296        memory_path: policy.memory_path,
2297        stale_warning_days: policy.stale_warning_days,
2298        retrieval_top_k: policy.retrieval_top_k,
2299        validation_enabled: policy.validation_enabled,
2300        max_content_bytes: policy.max_content_bytes,
2301        max_name_length: policy.max_name_length,
2302    }
2303}
2304
2305fn next_archived_seq_start(events: Option<&[SessionEntry]>) -> u64 {
2306    let mut next = 0u64;
2307    for entry in events.unwrap_or_default() {
2308        if let SessionEvent::Compressed {
2309            archived_seq_range, ..
2310        } = &entry.event
2311        {
2312            next = next.max(archived_seq_range.1 + 1);
2313        }
2314    }
2315    next
2316}
2317
2318fn rendered_context_from_messages(
2319    messages: Vec<Message>,
2320) -> deepstrike_core::context::renderer::RenderedContext {
2321    let mut system_parts = Vec::new();
2322    let mut turns = Vec::new();
2323    for message in messages {
2324        if message.role == deepstrike_core::types::message::Role::System {
2325            if let Some(text) = message.content.as_text() {
2326                system_parts.push(text.to_owned());
2327            }
2328        } else {
2329            turns.push(message);
2330        }
2331    }
2332    let system_text = system_parts.join("\n\n");
2333    deepstrike_core::context::renderer::RenderedContext {
2334        system_text: system_text.clone(),
2335        system_stable: system_text,
2336        system_knowledge: String::new(),
2337        turns,
2338        state_turn: None,
2339        frozen_prefix_len: None,
2340    }
2341}
2342
2343fn parse_update_plan_args(val: &serde_json::Value) -> TaskUpdate {
2344    let plan = val.get("plan").and_then(|v| {
2345        v.as_array().map(|arr| {
2346            arr.iter()
2347                .filter_map(|x| x.as_str().map(|s| s.to_string()))
2348                .collect()
2349        })
2350    });
2351    let current_step = val
2352        .get("current_step")
2353        .or_else(|| val.get("currentStep"))
2354        .and_then(|v| v.as_u64().map(|x| x as usize));
2355    let progress = val
2356        .get("progress")
2357        .and_then(|v| v.as_str().map(|s| s.to_string()));
2358    let scratchpad = val
2359        .get("scratchpad")
2360        .and_then(|v| v.as_str().map(|s| s.to_string()));
2361    let blocked_on = val
2362        .get("blocked_on")
2363        .or_else(|| val.get("blockedOn"))
2364        .and_then(|v| {
2365            v.as_array().map(|arr| {
2366                arr.iter()
2367                    .filter_map(|x| x.as_str().map(|s| s.to_string()))
2368                    .collect()
2369            })
2370        });
2371    let preserved_refs = val
2372        .get("preserved_refs")
2373        .or_else(|| val.get("preservedRefs"))
2374        .and_then(|v| {
2375            v.as_array().map(|arr| {
2376                arr.iter()
2377                    .filter_map(|x| x.as_str().map(|s| s.to_string()))
2378                    .collect()
2379            })
2380        });
2381    TaskUpdate {
2382        plan,
2383        current_step,
2384        progress,
2385        scratchpad,
2386        blocked_on,
2387        preserved_refs,
2388        // Directives are promoted in-kernel from acted-on signals; the SDK update path leaves them
2389        // untouched here (use `..` semantics) unless a future control plane curates them explicitly.
2390        directives: None,
2391    }
2392}
2393
2394fn select_memories(
2395    query: &MemoryQuery,
2396    entries: &[deepstrike_core::memory::semantic::MemoryEntry],
2397) -> MemoryRetrieval {
2398    let filter_out: std::collections::HashSet<String> = query
2399        .already_surfaced
2400        .iter()
2401        .chain(query.active_tools.iter())
2402        .cloned()
2403        .collect();
2404    let candidates: Vec<_> = entries
2405        .iter()
2406        .filter(|entry| {
2407            entry
2408                .metadata
2409                .get("name")
2410                .and_then(|value| value.as_str())
2411                .is_none_or(|name| !filter_out.contains(name))
2412        })
2413        .collect();
2414    if candidates.is_empty() {
2415        return MemoryRetrieval {
2416            selected_memory_ids: vec![],
2417            selection_rationale: "No candidates after filtering".to_string(),
2418        };
2419    }
2420    MemoryRetrieval {
2421        selected_memory_ids: candidates
2422            .iter()
2423            .take(query.top_k)
2424            .filter_map(|entry| {
2425                entry
2426                    .metadata
2427                    .get("name")
2428                    .and_then(|value| value.as_str())
2429                    .map(str::to_string)
2430            })
2431            .collect(),
2432        selection_rationale: "Stub selector ranked index entries".to_string(),
2433    }
2434}