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