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