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