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