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