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 has_attempted_reactive_compact = false;
837 let mut active_skill: Option<String> = None;
839 let session_start_ms = std::time::SystemTime::now()
840 .duration_since(std::time::UNIX_EPOCH)
841 .unwrap_or_default()
842 .as_millis() as u64;
843
844 let os_profile = assert_native_profile(self.opts.os_profile.clone())?;
845 let governance_policy = self
846 .opts
847 .governance_policy
848 .clone()
849 .unwrap_or(os_profile.governance_policy);
850 kernel_apply(
851 &mut kernel,
852 &mut pending_observations,
853 governance_policy.into_kernel_event(),
854 );
855
856 let attention_policy = self
857 .opts
858 .attention_policy
859 .unwrap_or(os_profile.attention_policy);
860 kernel_apply(
861 &mut kernel,
862 &mut pending_observations,
863 KernelInputEvent::SetAttentionPolicy {
864 max_queue_size: attention_policy.max_queue_size.unwrap_or(64),
865 },
866 );
867
868 if let Some(max_wall_ms) = effective_wall_budget {
869 kernel_apply(
870 &mut kernel,
871 &mut pending_observations,
872 KernelInputEvent::SetSchedulerBudget {
873 max_wall_ms: Some(max_wall_ms),
874 },
875 );
876 }
877
878 if let Some(quota) = self.opts.resource_quota.clone() {
879 kernel_apply(
880 &mut kernel,
881 &mut pending_observations,
882 KernelInputEvent::SetResourceQuota { quota },
883 );
884 }
885
886 if let Some(policy) = self.opts.memory_policy.clone() {
887 kernel_apply(
888 &mut kernel,
889 &mut pending_observations,
890 memory_policy_event(policy),
891 );
892 }
893
894 if !resume_mid_run {
898 if let (Some(pre), Some(store), Some(agent_id)) = (
899 self.opts.pre_query_memory.clone(),
900 self.opts.dream_store.as_ref(),
901 self.opts.agent_id.as_deref(),
902 ) {
903 let queries = pre(goal.as_str());
904 let mut entries: Vec<deepstrike_core::mm::PageInEntry> = Vec::new();
905 for q in &queries {
906 let qt = q.trim();
907 if qt.is_empty() {
908 continue;
909 }
910 if let Ok(hits) = store.search(agent_id, qt, 5).await {
911 for hit in hits {
912 entries.push(deepstrike_core::mm::PageInEntry {
913 content: format!("[memory score={:.3}] {}", hit.score, hit.text),
914 tokens: None,
915 source: Some("memory".to_string()),
916 });
917 }
918 }
919 }
920 if !entries.is_empty() {
921 kernel_apply(
922 &mut kernel,
923 &mut pending_observations,
924 KernelInputEvent::PageIn { entries },
925 );
926 }
927 }
928 }
929
930 let mut action = if resume_mid_run {
931 kernel_action(
932 &mut kernel,
933 &mut pending_observations,
934 KernelInputEvent::Resume {
935 approved_calls: vec![],
936 denied_calls: vec![],
937 },
938 )
939 } else {
940 let run_spec = build_run_spec(
943 self.opts.run_spec.clone(),
944 self.opts.allowed_tool_ids.as_deref(),
945 self.opts.agent_id.as_deref(),
946 &session_id,
947 &goal,
948 );
949 kernel_action(
950 &mut kernel,
951 &mut pending_observations,
952 KernelInputEvent::StartRun {
953 task: RuntimeTask::new(&goal).with_criteria(criteria),
954 run_spec,
955 },
956 )
957 };
958
959 let mut last_skill_version: u64 = skill_watcher.as_ref().map(|w| w.version()).unwrap_or(0);
960
961 while !kernel.lock().unwrap().is_terminal() {
962 if let KernelAction::ExecuteTool { .. } = &action {
963 self.apply_kernel_page_in(
964 &session_id,
965 &kernel,
966 &mut pending_observations,
967 )
968 .await;
969 }
970
971 if let (Some(watcher), Some(skill_dir)) =
973 (&skill_watcher, &self.opts.skill_dir)
974 {
975 let cur = watcher.version();
976 if cur != last_skill_version {
977 last_skill_version = cur;
978 kernel_apply(
979 &mut kernel,
980 &mut pending_observations,
981 KernelInputEvent::SetAvailableSkills {
982 skills: scan_skill_dir(skill_dir),
983 },
984 );
985 }
986 }
987
988 next_archive_start = self
989 .append_observations(
990 &session_id,
991 &kernel,
992 &mut pending_observations,
993 &mut pending_spool_outputs,
994 next_archive_start,
995 )
996 .await;
997
998 if self.interrupted.load(Ordering::Relaxed) {
999 kernel_apply(
1000 &mut kernel,
1001 &mut pending_observations,
1002 KernelInputEvent::Timeout,
1003 );
1004 break;
1005 }
1006
1007 if let Some(ss) = &self.opts.signal_source {
1008 if let Some(sdk_sig) = ss.next_signal().await? {
1009 let urgency = match sdk_sig.kind.as_str() {
1010 "interrupt" => Urgency::Critical,
1011 _ => Urgency::Normal,
1012 };
1013 let kernel_sig = KernelSignal::new(
1014 KernelSignalSource::Custom,
1015 KernelSignalType::Event,
1016 urgency,
1017 sdk_sig.kind.as_str(),
1018 )
1019 .with_payload(sdk_sig.payload.clone())
1020 .with_timestamp(
1021 std::time::SystemTime::now()
1022 .duration_since(std::time::UNIX_EPOCH)
1023 .unwrap_or_default()
1024 .as_millis() as u64,
1025 );
1026 let executing = matches!(action, KernelAction::ExecuteTool { .. });
1027 let disposition = router.ingest(kernel_sig, executing);
1028 match disposition {
1029 SignalDisposition::InterruptNow | SignalDisposition::Interrupt => {
1030 if matches!(disposition, SignalDisposition::InterruptNow) {
1032 self.interrupted.store(true, Ordering::Relaxed);
1033 }
1034 kernel_apply(
1035 &mut kernel,
1036 &mut pending_observations,
1037 KernelInputEvent::Timeout,
1038 );
1039 break;
1040 }
1041 _ => {}
1042 }
1043 }
1044 }
1045
1046 let mut queued = router.next();
1047 while let Some(sig) = queued {
1048 if sig.urgency == Urgency::Critical {
1049 kernel_apply(
1050 &mut kernel,
1051 &mut pending_observations,
1052 KernelInputEvent::Timeout,
1053 );
1054 break;
1055 }
1056 queued = router.next();
1057 }
1058 if kernel.lock().unwrap().is_terminal() {
1059 break;
1060 }
1061
1062 match &action {
1063 KernelAction::CallProvider { context, tools } => {
1064 let mut final_text = String::new();
1065 let mut final_tool_calls: Vec<ToolCall> = Vec::new();
1066 let mut turn_tokens: u32 = 0;
1067 let mut turn_input_tokens: u32 = 0;
1068 let mut turn_cache_read_tokens: u32 = 0;
1069 let mut turn_cache_creation_tokens: u32 = 0;
1070 let mut turn_cache_read_by_slot: Option<crate::providers::CacheReadBySlot> = 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 if is_prompt_too_long_error(&e)
1107 && !has_attempted_reactive_compact
1108 {
1109 has_attempted_reactive_compact = true;
1110 let compact_step = kernel.lock().unwrap().step(KernelInput::new(
1111 KernelInputEvent::ForceCompact,
1112 ));
1113 let compacted = compact_step.observations.iter().any(|obs| {
1114 matches!(obs, KernelObservation::Compressed { .. })
1115 });
1116 pending_observations.extend(compact_step.observations);
1117 if compacted {
1118 next_archive_start = self
1119 .append_observations(
1120 &session_id,
1121 &kernel,
1122 &mut pending_observations,
1123 &mut pending_spool_outputs,
1124 next_archive_start,
1125 )
1126 .await;
1127 action = KernelAction::CallProvider {
1128 context: kernel.lock().unwrap().state_machine().ctx.render(),
1129 tools: tools.clone(),
1130 };
1131 continue;
1132 }
1133 }
1134 yield RunEvent::Error(e.to_string());
1135 kernel_apply(
1136 &mut kernel,
1137 &mut pending_observations,
1138 KernelInputEvent::Timeout,
1139 );
1140 break;
1141 }
1142 };
1143
1144 while let Some(evt) = provider_stream.next().await {
1145 match evt? {
1146 StreamEvent::TextDelta { delta } => {
1147 final_text.push_str(&delta);
1148 yield RunEvent::TextDelta(delta);
1149 }
1150 StreamEvent::ThinkingDelta { delta } => {
1151 yield RunEvent::ThinkingDelta(delta);
1152 }
1153 StreamEvent::ToolCall { id, name, arguments } => {
1154 yield RunEvent::ToolCall { id: id.clone(), name: name.clone() };
1155 final_tool_calls.push(ToolCall {
1156 id: compact_str::CompactString::new(&id),
1157 name: compact_str::CompactString::new(&name),
1158 arguments,
1159 });
1160 }
1161 StreamEvent::Usage {
1162 total_tokens,
1163 input_tokens,
1164 cache_read_input_tokens,
1165 cache_creation_input_tokens,
1166 cache_read_input_tokens_by_slot,
1167 ..
1168 } => {
1169 turn_tokens = total_tokens;
1170 turn_input_tokens = input_tokens;
1172 turn_cache_read_tokens = cache_read_input_tokens;
1173 turn_cache_creation_tokens = cache_creation_input_tokens;
1174 turn_cache_read_by_slot = cache_read_input_tokens_by_slot;
1175 }
1176 StreamEvent::Done => {}
1177 }
1178 }
1179
1180 let mut assistant = Message {
1181 role: deepstrike_core::types::message::Role::Assistant,
1182 content: deepstrike_core::types::message::Content::Text(final_text.clone()),
1183 tool_calls: final_tool_calls.clone(),
1184 token_count: if turn_tokens > 0 { Some(turn_tokens) } else { None },
1185 };
1186
1187 self.opts.provider.commit_stream_replay(&final_text, &final_tool_calls);
1188 let mut provider_replay = peek_provider_replay(
1189 self.opts.provider.as_ref(),
1190 &final_text,
1191 &final_tool_calls,
1192 );
1193 repair_llm_completed(&mut assistant, &mut provider_replay);
1194
1195 action = kernel_action(
1196 &mut kernel,
1197 &mut pending_observations,
1198 KernelInputEvent::ProviderResult {
1199 message: assistant.clone(),
1200 observed_input_tokens: None,
1201 observed_output_tokens: None,
1202 now_ms: None,
1205 },
1206 );
1207 self.log(
1208 &session_id,
1209 SessionEvent::LlmCompleted {
1210 turn: kernel.lock().unwrap().state_machine().turn,
1211 message: assistant,
1212 provider_replay,
1213 },
1214 )
1215 .await;
1216
1217 if let Some(ref sink) = self.opts.on_turn_metrics {
1221 sink(TurnMetrics {
1222 turn: kernel.lock().unwrap().state_machine().turn,
1223 tools_exposed,
1224 tools_called: final_tool_calls.len(),
1225 active_skill: active_skill.clone(),
1226 input_tokens: turn_input_tokens,
1227 cache_read_tokens: turn_cache_read_tokens,
1228 cache_creation_tokens: turn_cache_creation_tokens,
1229 cache_read_tokens_by_slot: turn_cache_read_by_slot.clone(),
1230 });
1231 }
1232 if let Some(skill_call) =
1233 final_tool_calls.iter().find(|c| c.name.as_str() == "skill")
1234 {
1235 if let Some(name) = skill_call.arguments.get("name").and_then(|v| v.as_str()) {
1236 active_skill = Some(name.to_string());
1237 }
1238 }
1239 }
1240 KernelAction::ExecuteTool { calls } => {
1241 let tool_calls = calls.clone();
1242 self.log(
1243 &session_id,
1244 SessionEvent::ToolRequested {
1245 turn: kernel.lock().unwrap().state_machine().turn,
1246 calls: tool_calls.clone(),
1247 },
1248 )
1249 .await;
1250
1251 if let Some(gov) = &self.opts.governance {
1252 let mut g = gov.lock().await;
1253 if let Some(aid) = &self.opts.agent_id {
1254 g.set_identity(aid, &session_id);
1255 }
1256 }
1257
1258 let run_ctx = RunContext {
1259 agent_id: self.opts.agent_id.as_deref(),
1260 skill_dir: self.opts.skill_dir.as_deref(),
1261 dream_store: self.opts.dream_store.as_deref(),
1262 knowledge_source: self.opts.knowledge_source.as_deref(),
1263 governance: self.opts.governance.clone(),
1264 on_tool_suspend: self.opts.on_tool_suspend.clone(),
1265 on_permission_request: self.opts.on_permission_request.clone(),
1266 };
1267
1268 let mut tool_results = Vec::new();
1269 let mut normal_calls = Vec::new();
1270 let mut plan_calls = Vec::new();
1271
1272 for call in &tool_calls {
1273 if call.name == "update_plan" {
1274 plan_calls.push(call);
1275 } else {
1276 normal_calls.push(call.clone());
1277 }
1278 }
1279
1280 for call in plan_calls {
1281 let update = parse_update_plan_args(&call.arguments);
1282 kernel_apply(
1283 &mut kernel,
1284 &mut pending_observations,
1285 KernelInputEvent::UpdateTask { update },
1286 );
1287 tool_results.push(deepstrike_core::types::message::ToolResult {
1288 call_id: call.id.clone(),
1289 output: deepstrike_core::types::message::Content::Text("success".to_string()),
1290 is_error: false,
1291 is_fatal: false,
1292 error_kind: None,
1293 token_count: None,
1294 });
1295 yield RunEvent::ToolResult {
1296 call_id: call.id.to_string(),
1297 content: "success".to_string(),
1298 is_error: false,
1299 is_fatal: false,
1300 error_kind: None,
1301 };
1302 }
1303
1304 if !normal_calls.is_empty() {
1305 let plane_stream = self.plane.execute_all(&normal_calls, run_ctx);
1306 let mut stream = plane_stream;
1307 while let Some(evt) = stream.next().await {
1308 match evt? {
1309 RunEvent::ToolResult {
1310 call_id,
1311 content,
1312 is_error,
1313 is_fatal,
1314 error_kind,
1315 } => {
1316 tool_results.push(deepstrike_core::types::message::ToolResult {
1317 call_id: compact_str::CompactString::new(&call_id),
1318 output: deepstrike_core::types::message::Content::Text(content),
1319 is_error,
1320 is_fatal,
1321 error_kind,
1322 token_count: None,
1323 });
1324 }
1325 RunEvent::ToolArgumentRepaired { call_id, name, original_arguments, repaired_arguments } => {
1326 self.log(
1327 &session_id,
1328 SessionEvent::ToolArgumentRepaired {
1329 turn: kernel.lock().unwrap().state_machine().turn,
1330 tool: name.clone(),
1331 original_arguments: original_arguments.clone(),
1332 repaired_arguments: repaired_arguments.clone(),
1333 },
1334 )
1335 .await;
1336 yield RunEvent::ToolArgumentRepaired {
1337 call_id,
1338 name,
1339 original_arguments,
1340 repaired_arguments,
1341 };
1342 }
1343 RunEvent::ToolDenied { call_id, tool_name, reason } => {
1344 self.log(
1345 &session_id,
1346 SessionEvent::ToolDenied {
1347 turn: kernel.lock().unwrap().state_machine().turn,
1348 call_id: call_id.clone(),
1349 tool_name: tool_name.clone(),
1350 reason: reason.clone(),
1351 },
1352 )
1353 .await;
1354 yield RunEvent::ToolDenied { call_id, tool_name, reason };
1355 }
1356 RunEvent::PermissionRequest { call_id, tool_name, arguments, reason } => {
1357 let turn = kernel.lock().unwrap().state_machine().turn;
1358 self.log(
1359 &session_id,
1360 SessionEvent::PermissionRequested {
1361 turn,
1362 tool: tool_name.clone(),
1363 arguments: arguments.clone(),
1364 reason: Some(reason.clone()),
1365 },
1366 )
1367 .await;
1368 yield RunEvent::PermissionRequest { call_id, tool_name, arguments, reason };
1369 }
1370 RunEvent::PermissionResolved { call_id, tool_name, approved, responder, reason } => {
1371 let turn = kernel.lock().unwrap().state_machine().turn;
1372 self.log(
1373 &session_id,
1374 SessionEvent::PermissionResolved {
1375 turn,
1376 approved,
1377 responder: responder.clone(),
1378 },
1379 )
1380 .await;
1381 yield RunEvent::PermissionResolved { call_id, tool_name, approved, responder, reason };
1382 }
1383 other => yield other,
1384 }
1385 }
1386 let names: Vec<String> = normal_calls.iter().map(|c| c.name.to_string()).collect();
1387 kernel_apply(
1388 &mut kernel,
1389 &mut pending_observations,
1390 KernelInputEvent::UpdateTask {
1391 update: TaskUpdate {
1392 progress: Some(format!("Executed tools: {}", names.join(", "))),
1393 ..Default::default()
1394 },
1395 },
1396 );
1397 }
1398
1399 self.log(
1400 &session_id,
1401 SessionEvent::ToolCompleted {
1402 turn: kernel.lock().unwrap().state_machine().turn,
1403 results: tool_results.clone(),
1404 },
1405 )
1406 .await;
1407
1408 for call in &normal_calls {
1409 if let Some(result) = tool_results
1410 .iter()
1411 .find(|r| r.call_id.as_str() == call.id.as_str())
1412 {
1413 let output = match &result.output {
1414 deepstrike_core::types::message::Content::Text(s) => s.to_string(),
1415 deepstrike_core::types::message::Content::Parts(parts) => {
1416 serde_json::to_string(parts).unwrap_or_default()
1417 }
1418 };
1419 pending_spool_outputs.insert(
1420 call.id.to_string(),
1421 (call.name.to_string(), output),
1422 );
1423 }
1424 }
1425
1426 for call in &tool_calls {
1429 if call.name.as_str() != "skill" {
1430 continue;
1431 }
1432 let ok = tool_results
1433 .iter()
1434 .any(|r| r.call_id.as_str() == call.id.as_str() && !r.is_error);
1435 if !ok {
1436 continue;
1437 }
1438 if let Some(name) = call.arguments.get("name").and_then(|v| v.as_str()) {
1439 kernel_apply(
1440 &mut kernel,
1441 &mut pending_observations,
1442 KernelInputEvent::SkillActivated { name: name.to_string() },
1443 );
1444 }
1445 }
1446
1447 action = kernel_action(
1448 &mut kernel,
1449 &mut pending_observations,
1450 KernelInputEvent::ToolResults {
1451 results: tool_results,
1452 },
1453 );
1454 }
1455 KernelAction::EvaluateMilestone {
1456 phase_id,
1457 criteria,
1458 required_evidence,
1459 ..
1460 } => {
1461 let policy = self.opts.milestone_policy;
1462 if policy == MilestonePolicy::AutoPass {
1463 let result = MilestoneCheckResult::pass(phase_id.clone());
1464 action = kernel_action(
1465 &mut kernel,
1466 &mut pending_observations,
1467 KernelInputEvent::MilestoneResult { result },
1468 );
1469 next_archive_start = self
1470 .append_observations(
1471 &session_id,
1472 &kernel,
1473 &mut pending_observations,
1474 &mut pending_spool_outputs,
1475 next_archive_start,
1476 )
1477 .await;
1478 } else if let Some(handler) = &self.opts.on_milestone_evaluate {
1479 let context = MilestoneEvaluationContext {
1480 phase_id: phase_id.clone(),
1481 criteria: criteria.clone(),
1482 required_evidence: required_evidence.clone(),
1483 };
1484 let check_future = handler(context);
1485 let result = check_future.await?;
1486 action = kernel_action(
1487 &mut kernel,
1488 &mut pending_observations,
1489 KernelInputEvent::MilestoneResult { result },
1490 );
1491 next_archive_start = self
1492 .append_observations(
1493 &session_id,
1494 &kernel,
1495 &mut pending_observations,
1496 &mut pending_spool_outputs,
1497 next_archive_start,
1498 )
1499 .await;
1500 } else {
1501 next_archive_start = self
1502 .append_observations(
1503 &session_id,
1504 &kernel,
1505 &mut pending_observations,
1506 &mut pending_spool_outputs,
1507 next_archive_start,
1508 )
1509 .await;
1510 self.log(
1511 &session_id,
1512 SessionEvent::RunTerminal {
1513 reason: "milestone_pending".to_string(),
1514 turns_used: kernel.lock().unwrap().state_machine().turn.max(1),
1515 total_tokens: 0,
1516 },
1517 )
1518 .await;
1519 yield RunEvent::Done {
1520 iterations: kernel.lock().unwrap().state_machine().turn.max(1),
1521 total_tokens: 0,
1522 status: "milestone_pending".to_string(),
1523 };
1524 return;
1525 }
1526 }
1527 KernelAction::Done { result } => {
1528 let status = format!("{:?}", result.termination).to_lowercase();
1529 let turns_used = result.turns_used.max(1);
1530 let total_tokens = result.total_tokens_used;
1531
1532 next_archive_start = self
1533 .append_observations(
1534 &session_id,
1535 &kernel,
1536 &mut pending_observations,
1537 &mut pending_spool_outputs,
1538 next_archive_start,
1539 )
1540 .await;
1541
1542 self.log(
1543 &session_id,
1544 SessionEvent::RunTerminal {
1545 reason: status.clone(),
1546 turns_used,
1547 total_tokens,
1548 },
1549 )
1550 .await;
1551
1552 if let (Some(store), Some(agent_id)) =
1553 (&self.opts.dream_store, &self.opts.agent_id)
1554 {
1555 let new_msgs = kernel.lock().unwrap().state_machine_mut().drain_new_messages();
1556 if !new_msgs.is_empty() {
1557 let now_ms = std::time::SystemTime::now()
1558 .duration_since(std::time::UNIX_EPOCH)
1559 .unwrap_or_default()
1560 .as_millis() as u64;
1561 let session = deepstrike_core::memory::durable::SessionData {
1562 session_id: uuid::Uuid::new_v4().to_string(),
1563 agent_id: agent_id.clone(),
1564 messages: new_msgs,
1565 metadata: serde_json::Value::Null,
1566 created_at_ms: session_start_ms,
1567 updated_at_ms: now_ms,
1568 };
1569 let _ = store.save_session(session).await;
1570 }
1571 }
1572
1573 yield RunEvent::Done {
1574 iterations: turns_used,
1575 total_tokens,
1576 status,
1577 };
1578 return;
1579 }
1580 }
1581 }
1582
1583 next_archive_start = self
1584 .append_observations(
1585 &session_id,
1586 &kernel,
1587 &mut pending_observations,
1588 &mut pending_spool_outputs,
1589 next_archive_start,
1590 )
1591 .await;
1592
1593 let (status, turns_used, total_tokens) = match &action {
1597 KernelAction::Done { result } => (
1598 format!("{:?}", result.termination).to_lowercase(),
1599 result.turns_used.max(1),
1600 result.total_tokens_used,
1601 ),
1602 _ => (
1603 if self.interrupted.load(Ordering::Relaxed) { "user_abort".to_string() } else { "error".to_string() },
1604 kernel.lock().unwrap().state_machine().turn.max(1),
1605 0,
1606 ),
1607 };
1608
1609 self.log(
1610 &session_id,
1611 SessionEvent::RunTerminal {
1612 reason: status.clone(),
1613 turns_used,
1614 total_tokens,
1615 },
1616 )
1617 .await;
1618
1619 if let KernelAction::Done { .. } = &action {
1620 if let (Some(store), Some(agent_id)) =
1621 (&self.opts.dream_store, &self.opts.agent_id)
1622 {
1623 let new_msgs = kernel.lock().unwrap().state_machine_mut().drain_new_messages();
1624 if !new_msgs.is_empty() {
1625 let now_ms = std::time::SystemTime::now()
1626 .duration_since(std::time::UNIX_EPOCH)
1627 .unwrap_or_default()
1628 .as_millis() as u64;
1629 let session = deepstrike_core::memory::durable::SessionData {
1630 session_id: uuid::Uuid::new_v4().to_string(),
1631 agent_id: agent_id.clone(),
1632 messages: new_msgs,
1633 metadata: serde_json::Value::Null,
1634 created_at_ms: session_start_ms,
1635 updated_at_ms: now_ms,
1636 };
1637 let _ = store.save_session(session).await;
1638 }
1639 }
1640 }
1641
1642 yield RunEvent::Done {
1643 iterations: turns_used,
1644 total_tokens,
1645 status,
1646 };
1647 }
1648 }
1649
1650 pub(crate) async fn append_observations(
1651 &self,
1652 session_id: &str,
1653 kernel_mutex: &std::sync::Mutex<KernelRuntime>,
1654 observations: &mut Vec<KernelObservation>,
1655 pending_spool_outputs: &mut std::collections::HashMap<String, (String, String)>,
1656 mut next_archive_start: u64,
1657 ) -> u64 {
1658 let drained = std::mem::take(observations);
1659 let (turn, preserved_refs, summary_tokens_by_index) = {
1660 let kernel = kernel_mutex.lock().unwrap();
1661 let sm = kernel.state_machine();
1662 let summary_tokens_by_index = drained
1663 .iter()
1664 .map(|obs| match obs {
1665 KernelObservation::Compressed { summary, .. } => {
1666 summary.as_ref().map(|s| sm.ctx.engine.count(s))
1667 }
1668 _ => None,
1669 })
1670 .collect::<Vec<_>>();
1671 (
1672 sm.turn,
1673 sm.ctx.partitions.task_state.preserved_refs.clone(),
1674 summary_tokens_by_index,
1675 )
1676 };
1677
1678 for (index, obs) in drained.into_iter().enumerate() {
1679 match obs {
1680 KernelObservation::Compressed {
1681 action,
1682 rho_after: _,
1683 summary,
1684 archived,
1685 invalidates_prefix_at: _,
1686 } => {
1687 let Some(log) = &self.opts.session_log else {
1688 continue;
1689 };
1690 let latest = log.latest_seq(session_id).await.unwrap_or(-1) as u64;
1691 if latest < next_archive_start {
1692 continue;
1693 }
1694 let end = latest;
1695
1696 let mut archive_ref = None;
1697 if let Some(store) = &self.opts.compression_store {
1698 if !archived.is_empty() {
1699 if let Ok(path_ref) =
1700 store.write(session_id, next_archive_start, &archived)
1701 {
1702 if !path_ref.is_empty() {
1703 archive_ref = Some(path_ref);
1704 }
1705 }
1706 }
1707 }
1708
1709 let summary_tokens = summary_tokens_by_index.get(index).copied().flatten();
1710 let action_str = match action {
1711 KernelPressureAction::None => "none".to_string(),
1712 KernelPressureAction::SnipCompact => "snip_compact".to_string(),
1713 KernelPressureAction::MicroCompact => "micro_compact".to_string(),
1714 KernelPressureAction::ContextCollapse => "context_collapse".to_string(),
1715 KernelPressureAction::AutoCompact => "auto_compact".to_string(),
1716 };
1717
1718 if let Ok(compressed_seq) = log
1719 .append(
1720 session_id,
1721 SessionEvent::Compressed {
1722 turn,
1723 archived_seq_range: (next_archive_start, end),
1724 category: Some(category_for_kind("compressed")),
1725 primitive: Some(primitive_for_kind("compressed")),
1726 action: Some(action_str),
1727 summary: summary.clone(),
1728 summary_tokens,
1729 archive_ref,
1730 preserved_refs: preserved_refs.clone(),
1731 },
1732 )
1733 .await
1734 {
1735 next_archive_start = compressed_seq + 1;
1736 }
1737 }
1738 KernelObservation::Rollbacked {
1739 turn,
1740 checkpoint_history_len,
1741 reason,
1742 } => {
1743 self.log(
1744 session_id,
1745 SessionEvent::Rollbacked {
1746 turn,
1747 category: Some(category_for_kind("rollbacked")),
1748 primitive: Some(primitive_for_kind("rollbacked")),
1749 checkpoint_history_len,
1750 reason,
1751 },
1752 )
1753 .await;
1754 }
1755 KernelObservation::CapabilityChanged {
1756 turn,
1757 added,
1758 removed,
1759 change_kind,
1760 capability_id,
1761 version,
1762 mounted_by,
1763 mount_reason,
1764 } => {
1765 self.log(
1766 session_id,
1767 SessionEvent::CapabilityChanged {
1768 turn,
1769 category: Some(category_for_kind("capability_changed")),
1770 primitive: Some(primitive_for_kind("capability_changed")),
1771 added,
1772 removed,
1773 change_kind,
1774 capability_id,
1775 version,
1776 mounted_by,
1777 mount_reason,
1778 },
1779 )
1780 .await;
1781 }
1782 KernelObservation::MilestoneAdvanced {
1783 turn,
1784 phase_id,
1785 capabilities_unlocked,
1786 } => {
1787 self.log(
1788 session_id,
1789 SessionEvent::MilestoneAdvanced {
1790 turn,
1791 category: Some(category_for_kind("milestone_advanced")),
1792 primitive: Some(primitive_for_kind("milestone_advanced")),
1793 phase_id,
1794 capabilities_unlocked,
1795 },
1796 )
1797 .await;
1798 }
1799 KernelObservation::MilestoneBlocked {
1800 turn,
1801 phase_id,
1802 reason,
1803 } => {
1804 self.log(
1805 session_id,
1806 SessionEvent::MilestoneBlocked {
1807 turn,
1808 category: Some(category_for_kind("milestone_blocked")),
1809 primitive: Some(primitive_for_kind("milestone_blocked")),
1810 phase_id,
1811 reason,
1812 },
1813 )
1814 .await;
1815 }
1816 KernelObservation::MilestoneEvidence {
1817 turn,
1818 phase_id,
1819 evidence,
1820 } => {
1821 self.log(
1822 session_id,
1823 SessionEvent::MilestoneEvidence {
1824 turn,
1825 category: Some(category_for_kind("milestone_evidence")),
1826 primitive: Some(primitive_for_kind("milestone_evidence")),
1827 phase_id,
1828 evidence,
1829 },
1830 )
1831 .await;
1832 }
1833 KernelObservation::Renewed { .. } => {}
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 });
2070 }
2071
2072 let remaining_k = top_k.saturating_sub(local_hits.len());
2073 if remaining_k > 0 {
2074 if let (Some(store), Some(agent_id)) = (&self.opts.dream_store, &self.opts.agent_id) {
2075 if let Ok(hits) = store.search(agent_id, &query, remaining_k).await {
2076 for hit in hits {
2077 entries.push(deepstrike_core::mm::PageInEntry {
2078 content: format!("[memory score={:.3}] {}", hit.score, hit.text),
2079 tokens: None,
2080 source: Some("memory".to_string()),
2081 });
2082 }
2083 }
2084 }
2085 }
2086 } else if tool == "knowledge" {
2087 if let Some(source) = &self.opts.knowledge_source {
2088 if let Ok(snippets) = source.retrieve(&query, top_k).await {
2089 for snippet in snippets {
2090 entries.push(deepstrike_core::mm::PageInEntry {
2091 content: snippet,
2092 tokens: None,
2093 source: Some("knowledge".to_string()),
2094 });
2095 }
2096 }
2097 }
2098 }
2099 }
2100
2101 if entries.is_empty() {
2102 return;
2103 }
2104
2105 let mut kernel = kernel_mutex.lock().unwrap();
2107 let turn = kernel.state_machine().turn;
2108 let step = kernel.step(KernelInput::new(KernelInputEvent::PageIn {
2109 entries: entries.clone(),
2110 }));
2111 observations.extend(step.observations);
2112
2113 self.log(
2115 session_id,
2116 SessionEvent::PageIn {
2117 turn,
2118 category: Some(category_for_kind("page_in")),
2119 primitive: Some(primitive_for_kind("page_in")),
2120 entry_count: entries.len() as u32,
2121 },
2122 )
2123 .await;
2124 }
2125
2126 async fn archive_semantic_page_out(&self, archived: Vec<Message>, action: Option<String>) {
2127 let (Some(store), Some(agent_id)) = (&self.opts.dream_store, &self.opts.agent_id) else {
2128 return;
2129 };
2130
2131 let summary = match self.summarize_for_long_term_memory(&archived).await {
2132 Ok(s) => s,
2133 Err(_) => return, };
2135
2136 if let Ok(existing) = store.load_memories(agent_id).await {
2137 let curation_result = deepstrike_core::memory::curator::CurationResult {
2138 to_add: vec![deepstrike_core::memory::semantic::MemoryEntry {
2139 text: summary,
2140 score: 1.0,
2141 metadata: serde_json::json!({
2142 "source": "semantic_page_out",
2143 "action": action,
2144 }),
2145 }],
2146 to_remove_indices: vec![],
2147 stats: deepstrike_core::memory::curator::CurationStats {
2148 insights_processed: 1,
2149 duplicates_removed: 0,
2150 conflicts_resolved: 0,
2151 entries_added: 1,
2152 },
2153 };
2154 let _ = store.commit(agent_id, curation_result, &existing).await;
2155 }
2156 }
2157
2158 async fn summarize_for_long_term_memory(&self, archived: &[Message]) -> crate::Result<String> {
2159 let transcript = archived
2160 .iter()
2161 .map(|m| {
2162 let role_str = match m.role {
2163 deepstrike_core::types::message::Role::System => "system",
2164 deepstrike_core::types::message::Role::User => "user",
2165 deepstrike_core::types::message::Role::Assistant => "assistant",
2166 deepstrike_core::types::message::Role::Tool => "tool",
2167 };
2168 let content_str = message_content_as_text(&m.content);
2169 format!("{}: {}", role_str, content_str)
2170 })
2171 .collect::<Vec<_>>()
2172 .join("\n");
2173
2174 let system_prompt_opt = self.opts.system_prompt.as_deref();
2175 let system_text = match system_prompt_opt {
2176 Some(sp) => format!(
2177 "{}\n\nSummarize the following conversation for long-term memory. Preserve key facts, decisions, and open questions.",
2178 sp
2179 ),
2180 None => "Summarize the following conversation for long-term memory. Preserve key facts, decisions, and open questions.".to_string(),
2181 };
2182
2183 let context = deepstrike_core::context::renderer::RenderedContext {
2184 system_text,
2185 system_stable: String::new(),
2186 system_knowledge: String::new(),
2187 turns: vec![deepstrike_core::types::message::Message {
2188 role: deepstrike_core::types::message::Role::User,
2189 content: deepstrike_core::types::message::Content::Text(transcript.clone()),
2190 tool_calls: vec![],
2191 token_count: None,
2192 }],
2193 state_turn: None,
2194 frozen_prefix_len: None,
2195 };
2196
2197 let synth_state = self.opts.provider.create_run_state();
2198 let mut stream = self
2199 .opts
2200 .provider
2201 .stream(&context, &[], None, synth_state.as_ref())
2202 .await?;
2203
2204 let mut synthesis_text = String::new();
2205 while let Some(evt) = stream.next().await {
2206 if let Ok(StreamEvent::TextDelta { delta }) = evt {
2207 synthesis_text.push_str(&delta);
2208 }
2209 }
2210
2211 let text = synthesis_text.trim();
2212 if text.is_empty() {
2213 Ok(transcript.chars().take(2000).collect())
2214 } else {
2215 Ok(text.to_string())
2216 }
2217 }
2218}
2219
2220fn message_content_as_text(content: &deepstrike_core::types::message::Content) -> String {
2221 match content {
2222 deepstrike_core::types::message::Content::Text(s) => s.clone(),
2223 deepstrike_core::types::message::Content::Parts(parts) => parts
2224 .iter()
2225 .filter_map(|p| match p {
2226 deepstrike_core::types::message::ContentPart::Text { text } => Some(text.as_str()),
2227 deepstrike_core::types::message::ContentPart::ToolResult { output, .. } => Some(output.as_str()),
2228 _ => None,
2229 })
2230 .collect::<Vec<_>>()
2231 .join("\n"),
2232 }
2233}
2234
2235fn kernel_apply(
2236 kernel: &mut KernelRuntime,
2237 pending_observations: &mut Vec<KernelObservation>,
2238 event: KernelInputEvent,
2239) {
2240 let step = kernel.step(KernelInput::new(event));
2241 pending_observations.extend(step.observations);
2242}
2243
2244fn kernel_action(
2245 kernel: &mut KernelRuntime,
2246 pending_observations: &mut Vec<KernelObservation>,
2247 event: KernelInputEvent,
2248) -> KernelAction {
2249 let mut step = kernel.step(KernelInput::new(event));
2250 pending_observations.append(&mut step.observations);
2251 take_single_action(step)
2252}
2253
2254fn take_single_action(mut step: KernelStep) -> KernelAction {
2255 step.actions
2256 .pop()
2257 .expect("kernel transition must return one action")
2258}
2259
2260pub async fn collect_text(
2261 mut stream: std::pin::Pin<Box<dyn futures::Stream<Item = Result<RunEvent>> + '_>>,
2262) -> Result<String> {
2263 let mut text = String::new();
2264 while let Some(evt) = stream.next().await {
2265 if let RunEvent::TextDelta(d) = evt? {
2266 text.push_str(&d);
2267 }
2268 }
2269 Ok(text)
2270}
2271
2272fn merge_extensions(
2273 base: Option<&serde_json::Value>,
2274 over: Option<&serde_json::Value>,
2275) -> Option<serde_json::Value> {
2276 match (base, over) {
2277 (Some(b), Some(o)) => {
2278 let mut merged = b.clone();
2279 if let (Some(m), Some(obj)) = (merged.as_object_mut(), o.as_object()) {
2280 for (k, v) in obj {
2281 m.insert(k.clone(), v.clone());
2282 }
2283 }
2284 Some(merged)
2285 }
2286 (Some(b), None) => Some(b.clone()),
2287 (None, Some(o)) => Some(o.clone()),
2288 (None, None) => None,
2289 }
2290}
2291
2292fn is_prompt_too_long_error(error: &Error) -> bool {
2293 let msg = error.to_string().to_lowercase();
2294 msg.contains("413")
2295 || msg.contains("too long")
2296 || msg.contains("prompt too long")
2297 || msg.contains("context length exceeded")
2298 || msg.contains("context_length_exceeded")
2299}
2300
2301fn effective_wall_budget(
2302 scheduler_budget: Option<SchedulerBudget>,
2303 fallback_timeout_ms: Option<u64>,
2304) -> Option<u64> {
2305 scheduler_budget
2306 .and_then(|budget| budget.max_wall_ms)
2307 .or(fallback_timeout_ms)
2308}
2309
2310fn memory_policy_event(policy: MemoryPolicy) -> KernelInputEvent {
2312 KernelInputEvent::SetMemoryPolicy {
2313 memory_path: policy.memory_path,
2314 stale_warning_days: policy.stale_warning_days,
2315 retrieval_top_k: policy.retrieval_top_k,
2316 validation_enabled: policy.validation_enabled,
2317 max_content_bytes: policy.max_content_bytes,
2318 max_name_length: policy.max_name_length,
2319 }
2320}
2321
2322fn next_archived_seq_start(events: Option<&[SessionEntry]>) -> u64 {
2323 let mut next = 0u64;
2324 for entry in events.unwrap_or_default() {
2325 if let SessionEvent::Compressed {
2326 archived_seq_range, ..
2327 } = &entry.event
2328 {
2329 next = next.max(archived_seq_range.1 + 1);
2330 }
2331 }
2332 next
2333}
2334
2335fn rendered_context_from_messages(
2336 messages: Vec<Message>,
2337) -> deepstrike_core::context::renderer::RenderedContext {
2338 let mut system_parts = Vec::new();
2339 let mut turns = Vec::new();
2340 for message in messages {
2341 if message.role == deepstrike_core::types::message::Role::System {
2342 if let Some(text) = message.content.as_text() {
2343 system_parts.push(text.to_owned());
2344 }
2345 } else {
2346 turns.push(message);
2347 }
2348 }
2349 let system_text = system_parts.join("\n\n");
2350 deepstrike_core::context::renderer::RenderedContext {
2351 system_text: system_text.clone(),
2352 system_stable: system_text,
2353 system_knowledge: String::new(),
2354 turns,
2355 state_turn: None,
2356 frozen_prefix_len: None,
2357 }
2358}
2359
2360fn parse_update_plan_args(val: &serde_json::Value) -> TaskUpdate {
2361 let plan = val.get("plan").and_then(|v| {
2362 v.as_array().map(|arr| {
2363 arr.iter()
2364 .filter_map(|x| x.as_str().map(|s| s.to_string()))
2365 .collect()
2366 })
2367 });
2368 let current_step = val
2369 .get("current_step")
2370 .or_else(|| val.get("currentStep"))
2371 .and_then(|v| v.as_u64().map(|x| x as usize));
2372 let progress = val
2373 .get("progress")
2374 .and_then(|v| v.as_str().map(|s| s.to_string()));
2375 let scratchpad = val
2376 .get("scratchpad")
2377 .and_then(|v| v.as_str().map(|s| s.to_string()));
2378 let blocked_on = val
2379 .get("blocked_on")
2380 .or_else(|| val.get("blockedOn"))
2381 .and_then(|v| {
2382 v.as_array().map(|arr| {
2383 arr.iter()
2384 .filter_map(|x| x.as_str().map(|s| s.to_string()))
2385 .collect()
2386 })
2387 });
2388 let preserved_refs = val
2389 .get("preserved_refs")
2390 .or_else(|| val.get("preservedRefs"))
2391 .and_then(|v| {
2392 v.as_array().map(|arr| {
2393 arr.iter()
2394 .filter_map(|x| x.as_str().map(|s| s.to_string()))
2395 .collect()
2396 })
2397 });
2398 TaskUpdate {
2399 plan,
2400 current_step,
2401 progress,
2402 scratchpad,
2403 blocked_on,
2404 preserved_refs,
2405 directives: None,
2408 }
2409}
2410
2411fn select_memories(
2412 query: &MemoryQuery,
2413 entries: &[deepstrike_core::memory::semantic::MemoryEntry],
2414) -> MemoryRetrieval {
2415 let filter_out: std::collections::HashSet<String> = query
2416 .already_surfaced
2417 .iter()
2418 .chain(query.active_tools.iter())
2419 .cloned()
2420 .collect();
2421 let candidates: Vec<_> = entries
2422 .iter()
2423 .filter(|entry| {
2424 entry
2425 .metadata
2426 .get("name")
2427 .and_then(|value| value.as_str())
2428 .is_none_or(|name| !filter_out.contains(name))
2429 })
2430 .collect();
2431 if candidates.is_empty() {
2432 return MemoryRetrieval {
2433 selected_memory_ids: vec![],
2434 selection_rationale: "No candidates after filtering".to_string(),
2435 };
2436 }
2437 MemoryRetrieval {
2438 selected_memory_ids: candidates
2439 .iter()
2440 .take(query.top_k)
2441 .filter_map(|entry| {
2442 entry
2443 .metadata
2444 .get("name")
2445 .and_then(|value| value.as_str())
2446 .map(str::to_string)
2447 })
2448 .collect(),
2449 selection_rationale: "Stub selector ranked index entries".to_string(),
2450 }
2451}