1use anyhow::Result;
31use oxi_sdk::observability::AuditTrail;
32use oxi_sdk::{
33 Agent, AgentConfig, AgentEvent, CompactionEvent, CompactionStrategy, ProviderResolver,
34};
35use oxi_sdk::{SearchCache, ToolExecutionMode, ToolRegistry};
36use parking_lot::Mutex;
37use std::sync::Arc;
38use crate::access_manager::{AccessGate, AgentContext, TracingAuditSink, TrailAuditSink};
42use crate::capability::resolve::resolve_cspace;
43use crate::engine::OxiosEngine;
44use crate::memory::{MemoryEntry, MemoryManager, MemoryType};
45use crate::persona::PersonaManager;
46use crate::tools::registration::register_tools_from_cspace_gated;
47
48use crate::event_bus::KernelEvent;
49use crate::session_context::SessionContext;
50use crate::types::AgentId;
51use crate::KernelHandle;
52use oxios_ouroboros::{ExecutionResult, Seed};
53
54static LLM_CIRCUIT_BREAKER: std::sync::OnceLock<oxi_sdk::ProviderCircuitBreaker> =
56 std::sync::OnceLock::new();
57
58fn get_llm_circuit_breaker() -> &'static oxi_sdk::ProviderCircuitBreaker {
60 LLM_CIRCUIT_BREAKER.get_or_init(|| {
61 oxi_sdk::ProviderCircuitBreaker::new(
62 "global".to_string(),
63 oxi_sdk::CircuitBreakerConfig::default(),
64 )
65 })
66}
67
68#[derive(Debug, Clone)]
70pub struct AgentRuntimeConfig {
71 pub model_id: String,
73 pub max_iterations: usize,
75 pub tool_execution: ToolExecutionMode,
77 pub auto_retry_enabled: bool,
79 pub project_paths: Vec<std::path::PathBuf>,
81 pub workspace_dir: Option<std::path::PathBuf>,
83 pub api_key: Option<String>,
85 pub provider_options: Option<oxi_sdk::ProviderOptions>,
87 pub rate_limit_per_minute: usize,
89 pub token_budget: usize,
91 pub audit_tool_calls: bool,
93 pub provider_rpm: u32,
96}
97
98impl Default for AgentRuntimeConfig {
99 fn default() -> Self {
100 Self {
101 model_id: String::new(),
102 max_iterations: 8,
103 tool_execution: ToolExecutionMode::Parallel,
104 auto_retry_enabled: true,
105 project_paths: Vec::new(),
106 workspace_dir: None,
107 api_key: None,
108 provider_options: None,
109 rate_limit_per_minute: 0,
110 token_budget: 0,
111 audit_tool_calls: false,
112 provider_rpm: 0,
113 }
114 }
115}
116
117#[derive(Default)]
119struct ExecuteState {
120 final_content: String,
121 steps_completed: usize,
122 success: bool,
123 trajectory_steps: Vec<oxios_memory::memory::sona::TrajectoryStep>,
127 pending_tools: std::collections::HashMap<String, (std::time::Instant, usize)>,
131}
132
133pub struct AgentRuntime {
142 engine_handle: Arc<crate::engine::EngineHandle>,
143 config: AgentRuntimeConfig,
144 kernel_handle: Arc<KernelHandle>,
146 persona_manager: Option<Arc<PersonaManager>>,
148 tool_retriever: Option<Arc<crate::tools::retrieval::ToolRetriever>>,
150 routing_stats: Option<Arc<crate::kernel_handle::RoutingStats>>,
152}
153
154impl AgentRuntime {
155 pub fn new(
160 engine_handle: Arc<crate::engine::EngineHandle>,
161 model_id: impl Into<String>,
162 kernel_handle: Arc<KernelHandle>,
163 routing_stats: Option<Arc<crate::kernel_handle::RoutingStats>>,
164 ) -> Self {
165 Self {
166 engine_handle,
167 config: AgentRuntimeConfig {
168 model_id: model_id.into(),
169 ..Default::default()
170 },
171 kernel_handle,
172 persona_manager: None,
173 tool_retriever: None,
174 routing_stats,
175 }
176 }
177
178 pub fn with_persona_manager(mut self, pm: Arc<PersonaManager>) -> Self {
180 self.persona_manager = Some(pm);
181 self
182 }
183
184 pub fn with_config(mut self, config: AgentRuntimeConfig) -> Self {
186 self.config = config;
187 self
188 }
189
190 pub fn with_tool_retriever(
192 mut self,
193 retriever: Arc<crate::tools::retrieval::ToolRetriever>,
194 ) -> Self {
195 self.tool_retriever = Some(retriever);
196 self
197 }
198
199 pub async fn execute(
207 &self,
208 agent_id: AgentId,
209 seed: &Seed,
210 session_ctx: &mut SessionContext,
211 ) -> Result<ExecutionResult> {
212 let session_id: Option<String> = Some(seed.id.to_string());
216 self.execute_with_session(agent_id, seed, session_ctx, session_id)
217 .await
218 }
219
220 pub async fn execute_with_session(
223 &self,
224 agent_id: AgentId,
225 seed: &Seed,
226 session_ctx: &mut SessionContext,
227 session_id: Option<String>,
228 ) -> Result<ExecutionResult> {
229 let prompt = build_user_prompt(seed);
230
231 let persona_prompt = self
233 .persona_manager
234 .as_ref()
235 .map(|pm| pm.active_system_prompt())
236 .filter(|s| !s.trim().is_empty());
237
238 let persona_role = self
240 .persona_manager
241 .as_ref()
242 .and_then(|pm| pm.get_active_persona().map(|p| p.role.clone()));
243
244 let cspace = resolve_cspace(
246 seed.cspace_hint.as_deref(),
247 persona_role.as_deref(),
248 Some("worker"),
249 agent_id,
250 );
251
252 let mut system_prompt = build_system_prompt(seed, persona_prompt.as_deref(), None, None);
255
256 let capabilities_xml = if let Some(ref retriever) = self.tool_retriever {
258 match retriever.embedder().embed(&seed.goal).await {
259 Ok(query_vec) => {
260 let results = retriever.retrieve(&query_vec, 8);
261 if results.is_empty() {
262 None
263 } else {
264 let xml = crate::tools::retrieval::format_capability_index(&results);
265 tracing::info!(count = results.len(), "Retrieved relevant capabilities");
266 Some(xml)
267 }
268 }
269 Err(e) => {
270 tracing::warn!(error = %e, "Failed to embed seed goal for retrieval");
271 None
272 }
273 }
274 } else {
275 None
276 };
277
278 let kernel_manifest = {
280 let domains = cspace.active_domains();
281 if domains.is_empty() {
282 None
283 } else {
284 Some(crate::tools::retrieval::build_kernel_manifest(&domains))
285 }
286 };
287
288 if capabilities_xml.is_some() || kernel_manifest.is_some() {
290 system_prompt = build_system_prompt(
291 seed,
292 persona_prompt.as_deref(),
293 capabilities_xml.as_deref(),
294 kernel_manifest.as_deref(),
295 );
296 }
297
298 let memory_manager = self.kernel_handle.agents.memory_manager();
300 match memory_manager
301 .recall_with_proactive(&seed.goal, &mut session_ctx.recall_timing)
302 .await
303 {
304 Ok(memories) if !memories.is_empty() => {
305 tracing::info!(count = memories.len(), "Recalled memories for seed");
306 system_prompt = memory_manager.blend_into_prompt(&memories, &system_prompt);
307 }
308 Ok(_) => tracing::debug!("No memories recalled"),
309 Err(e) => tracing::warn!(error = %e, "Failed to recall memories"),
310 }
311
312 if let Some(sona) = memory_manager.sona_engine() {
314 match sona.adapt(&seed.goal).await {
315 Ok(Some(pattern)) if pattern.confidence > 0.5 => {
316 tracing::info!(
317 domain = %pattern.domain,
318 confidence = pattern.confidence,
319 "SONA learned pattern injected"
320 );
321 system_prompt.push_str(&format!(
322 "\n\n## Learned Strategy (confidence: {:.0}%)\n{}\n",
323 pattern.confidence * 100.0,
324 pattern.strategy,
325 ));
326 }
327 Ok(_) => tracing::debug!("No high-confidence SONA pattern found"),
328 Err(e) => tracing::debug!(error = %e, "SONA adapt failed (non-fatal)"),
329 }
330 }
331
332 match self
334 .kernel_handle
335 .knowledge_lens
336 .recall_for_context(&seed.goal, 5)
337 .await
338 {
339 Ok(ctx) if !ctx.notes.is_empty() => {
340 tracing::info!(
341 notes = ctx.notes.len(),
342 memories = ctx.memories.len(),
343 "Recalled knowledge context for seed"
344 );
345 let knowledge_blend = ctx
346 .notes
347 .iter()
348 .take(3)
349 .map(|n| format!("## {}\n\n{}", n.name, n.content))
350 .collect::<Vec<_>>()
351 .join("\n\n");
352 system_prompt.push_str("\n\n## Relevant Knowledge\n\n");
353 system_prompt.push_str(&knowledge_blend);
354 }
355 Ok(_) => tracing::debug!("No knowledge recalled"),
356 Err(e) => tracing::warn!(error = %e, "Failed to recall knowledge context"),
357 }
358
359 let engine = self.engine_handle.get();
362 let _model = engine.resolve_model(&self.config.model_id)?;
363 let seed_id = seed.id;
364
365 let config = self.config.clone();
367 let kernel_handle = Arc::clone(&self.kernel_handle);
368
369 let audit_trail: Option<Arc<AuditTrail>> =
371 Some(Arc::clone(&self.kernel_handle.security.audit_trail));
372
373 let (final_content, steps_completed, success, trajectory_steps, _agent) = {
374 run_agent(
375 &config,
376 &engine,
377 kernel_handle,
378 system_prompt,
379 prompt,
380 seed_id,
381 seed.goal.clone(),
382 agent_id,
383 cspace,
384 audit_trail,
385 self.routing_stats.clone(),
386 session_id.clone(),
387 )
388 .await?
389 };
390
391 let tool_calls: Vec<oxios_ouroboros::ToolCallRecord> = trajectory_steps
393 .iter()
394 .map(|s| oxios_ouroboros::ToolCallRecord {
395 tool: s.input.clone(),
396 input: String::new(), output: s.output.clone(),
398 duration_ms: s.duration_ms,
399 })
400 .collect();
401
402 tracing::info!(
403 seed_id = %seed_id,
404 steps = steps_completed,
405 success,
406 tool_calls = tool_calls.len(),
407 "AgentRuntime finished"
408 );
409
410 Ok(ExecutionResult {
411 output: if final_content.is_empty() {
412 "Agent execution completed".into()
413 } else {
414 final_content
415 },
416 steps_completed,
417 success,
418 tool_calls,
419 })
420 }
421}
422
423#[allow(clippy::too_many_arguments)]
428async fn run_agent(
429 config: &AgentRuntimeConfig,
430 engine: &OxiosEngine,
431 kernel_handle: Arc<KernelHandle>,
432 system_prompt: String,
433 prompt: String,
434 seed_id: uuid::Uuid,
435 seed_goal: String,
436 agent_id: AgentId,
437 cspace: crate::capability::CSpace,
438 audit_trail: Option<Arc<AuditTrail>>,
439 routing_stats: Option<Arc<crate::kernel_handle::RoutingStats>>,
440 session_id: Option<String>,
441) -> Result<(
442 String,
443 usize,
444 bool,
445 Vec<oxios_memory::memory::sona::TrajectoryStep>,
446 Arc<Agent>,
447)> {
448 let workspace = if !config.project_paths.is_empty() {
450 config.project_paths[0].clone()
451 } else if let Some(ref ws) = config.workspace_dir {
452 ws.clone()
453 } else {
454 std::env::temp_dir()
455 .join("oxios-agent-workspace")
456 .join(agent_id.to_string())
457 };
458
459 let _ = std::fs::create_dir_all(&workspace);
461
462 tracing::debug!(workspace = %workspace.display(), "Agent workspace scoped");
463
464 let _trace_guard = crate::observability::tracer().start(
466 format!("seed-{}", &seed_id.to_string()[..8]).as_str(),
467 oxi_sdk::SpanKind::Agent,
468 );
469
470 let registry = ToolRegistry::new();
472 let search_cache = Arc::new(SearchCache::new());
473
474 let agent_context = AgentContext {
476 agent_id,
477 agent_name: format!("agent-{agent_id}"),
478 cspace: Arc::new(cspace.clone()),
479 };
480
481 let audit_sink: Arc<dyn crate::access_manager::AuditSink> = if let Some(trail) = audit_trail {
484 let audit_path = kernel_handle
485 .state
486 .workspace_path()
487 .join("audit")
488 .join("access.jsonl");
489 Arc::new(TrailAuditSink::new(trail, audit_path))
490 } else {
491 Arc::new(TracingAuditSink)
492 };
493
494 let access_gate = Arc::new(AccessGate::new(
496 kernel_handle.exec.access_manager().clone(),
497 Arc::new(kernel_handle.exec.config_snapshot()),
498 audit_sink,
499 ));
500
501 register_tools_from_cspace_gated(
502 ®istry,
503 &kernel_handle,
504 &cspace,
505 search_cache,
506 agent_id,
507 access_gate,
508 agent_context,
509 );
510
511 tracing::info!(
512 seed_id = %seed_id,
513 capabilities = cspace.len(),
514 "Tools registered from CSpace"
515 );
516
517 let agent_config = AgentConfig {
525 name: format!("agent-{agent_id}"),
526 description: None,
527 model_id: config.model_id.clone(),
528 system_prompt: Some(system_prompt.clone()),
529 max_iterations: config.max_iterations,
530 timeout_seconds: 300,
531 temperature: Some(0.7),
532 max_tokens: Some(8192),
533 compaction_strategy: CompactionStrategy::Threshold(0.8),
534 compaction_instruction: None,
535 context_window: 128_000,
536 api_key: config.api_key.clone(),
537 workspace_dir: config.project_paths.first().cloned(),
538 output_mode: None,
539 provider_options: config.provider_options.clone(),
540 };
541
542 let agent = if config.provider_rpm > 0 {
557 let resolver: Arc<dyn ProviderResolver> = Arc::new(engine.oxi().clone());
559 let provider_name = engine.resolve_model(&config.model_id)?.provider;
560 let provider = engine.pooled_provider(&provider_name, config.provider_rpm)?;
561
562 let mut pipeline = oxi_sdk::MiddlewarePipeline::new();
564 if config.rate_limit_per_minute > 0 {
565 pipeline = pipeline.push(oxi_sdk::middleware::builtins::RateLimitMiddleware::new(
566 config.rate_limit_per_minute,
567 ));
568 }
569 if config.token_budget > 0 {
570 pipeline = pipeline.push(oxi_sdk::middleware::builtins::TokenBudgetMiddleware::new(
571 config.token_budget,
572 ));
573 }
574 if config.audit_tool_calls {
575 pipeline = pipeline.push(oxi_sdk::middleware::builtins::LoggingMiddleware::new(
576 tracing::Level::INFO,
577 ));
578 }
579
580 let agent = Arc::new(Agent::new_with_resolver(
582 provider,
583 agent_config,
584 Arc::new(registry),
585 resolver,
586 ));
587
588 if !pipeline.is_empty() {
590 let terminate_flag = Arc::new(std::sync::atomic::AtomicBool::new(false));
591 let agent_id_for_hooks = agent_id.to_string();
592 let hooks = oxi_sdk::middleware::build_hooks(
593 Arc::new(pipeline),
594 agent_id_for_hooks,
595 terminate_flag,
596 );
597 agent.set_hooks(hooks);
598 }
599
600 agent
601 } else {
602 let mut builder = engine
604 .oxi()
605 .agent(agent_config)
606 .workspace(&workspace)
607 .system_prompt(system_prompt);
608
609 let cspace_tool_arcs: Vec<Arc<dyn oxi_sdk::AgentTool>> = registry
619 .names()
620 .into_iter()
621 .filter_map(|name| registry.get(&name))
622 .collect();
623
624 if let Some(auth) = engine.authorizer() {
626 builder = builder.authorizer(auth.clone());
627 }
628 if let Some(tracer) = engine.tracer() {
629 builder = builder.tracer(tracer.clone());
630 }
631 if let Some(ct) = engine.cost_tracker() {
632 builder = builder.cost_tracker(ct.clone());
633 }
634
635 if config.rate_limit_per_minute > 0 {
638 builder = builder.with_rate_limit(config.rate_limit_per_minute);
639 }
640 if config.token_budget > 0 {
641 builder = builder.with_token_budget(config.token_budget);
642 }
643 if config.audit_tool_calls {
644 builder = builder.with_logging();
645 }
646
647 let built = builder.build()?;
648 let agent = Arc::new(built);
649
650 let agent_tools = agent.tools();
655 for tool in cspace_tool_arcs {
656 agent_tools.register_arc(tool);
657 }
658
659 agent
660 };
661
662 let exec_state = Arc::new(Mutex::new(ExecuteState::default()));
664 let exec_state_cb = Arc::clone(&exec_state);
665 let memory_for_callback: Arc<MemoryManager> = (*kernel_handle.agents.memory_manager()).clone();
666 let session_id_for_callback = seed_id.to_string();
667 let model_id_for_callback = config.model_id.clone();
668 let agent_id_for_callback = agent_id.to_string();
669 let routing_stats_for_cb = routing_stats.clone();
670 let transparency_session: Option<String> = session_id.clone();
673 let kernel_handle_for_cb: Arc<KernelHandle> = Arc::clone(&kernel_handle);
674
675 let result = agent
677 .run_streaming(prompt, move |event| {
678 let mut s = exec_state_cb.lock();
679 match event {
680 AgentEvent::ToolExecutionStart {
681 tool_name,
682 tool_call_id,
683 ..
684 } => {
685 let idx = s.trajectory_steps.len();
687 s.pending_tools
688 .insert(tool_call_id.clone(), (std::time::Instant::now(), idx));
689 s.trajectory_steps
690 .push(oxios_memory::memory::sona::TrajectoryStep {
691 input: tool_name.clone(),
692 output: String::new(),
693 duration_ms: 0,
694 confidence: 0.0,
695 });
696 if let Some(ref sid) = transparency_session {
698 let _ =
699 kernel_handle_for_cb
700 .infra
701 .publish(KernelEvent::ToolExecutionStarted {
702 session_id: sid.clone(),
703 tool_name: tool_name.clone(),
704 tool_call_id: tool_call_id.clone(),
705 tool_args: serde_json::Value::Null,
706 });
707 }
708 }
709 AgentEvent::ToolExecutionUpdate {
710 tool_call_id,
711 tool_name,
712 partial_result,
713 tab_id,
714 context,
715 } => {
716 if let Some(ref sid) = transparency_session {
726 let context_json = context
727 .as_ref()
728 .map(serde_json::to_value)
729 .transpose()
730 .unwrap_or(None);
731 let _ = kernel_handle_for_cb.infra.publish(
732 KernelEvent::ToolExecutionProgress {
733 session_id: sid.clone(),
734 tool_call_id: tool_call_id.clone(),
735 tool_name: tool_name.clone(),
736 progress: partial_result,
737 tab_id,
738 context: context_json,
739 },
740 );
741 }
742 }
743 AgentEvent::ToolExecutionEnd {
744 tool_name,
745 tool_call_id,
746 is_error,
747 result,
748 ..
749 } => {
750 if !is_error {
751 s.steps_completed += 1;
752 }
753 let mut duration_ms: u64 = 0;
755 let mut summary = String::new();
756 if let Some((start, idx)) = s.pending_tools.remove(tool_call_id.as_str()) {
757 duration_ms = start.elapsed().as_millis() as u64;
758 if let Some(step) = s.trajectory_steps.get_mut(idx) {
759 summary = summarize_tool_result(&result.content, 200);
760 step.output = summary.clone();
761 step.duration_ms = duration_ms;
762 step.confidence = if is_error { 0.3 } else { 0.8 };
763 }
764 }
765 if let Some(ref sid) = transparency_session {
767 let _ = kernel_handle_for_cb.infra.publish(
768 KernelEvent::ToolExecutionFinished {
769 session_id: sid.clone(),
770 tool_call_id: tool_call_id.clone(),
771 tool_name: tool_name.clone(),
772 duration_ms,
773 is_error,
774 output_summary: summary,
775 },
776 );
777 }
778 }
779 AgentEvent::AgentEnd {
780 messages,
781 stop_reason,
782 ..
783 } => {
784 if let Some(oxi_sdk::Message::Assistant(a)) = messages.last() {
785 s.final_content = a.text_content();
786 }
787 s.success = stop_reason.as_deref() == Some("Stop");
788 }
789 AgentEvent::Error { message, .. } => {
790 s.final_content = message.clone();
791 s.success = false;
792 }
793 AgentEvent::Usage {
794 input_tokens,
795 output_tokens,
796 } => {
797 let agent_label = format!("agent-{agent_id_for_callback}");
799 crate::observability::cost_tracker().record(
800 &agent_label,
801 &oxi_sdk::Model::new(
802 &model_id_for_callback,
803 &model_id_for_callback,
804 oxi_sdk::Api::OpenAiCompletions,
805 "unknown",
806 "https://unknown.com",
807 ),
808 oxi_sdk::TokenUsage {
809 input: input_tokens as u64,
810 output: output_tokens as u64,
811 cache_read: 0,
812 cache_write: 0,
813 },
814 );
815
816 if let Some(stats) = &routing_stats_for_cb {
818 let cost = crate::kernel_handle::engine_api::estimate_cost(
819 &model_id_for_callback,
820 input_tokens as u64,
821 output_tokens as u64,
822 );
823 stats.record_model_usage(&model_id_for_callback, cost);
824 }
825 if let Some(ref sid) = transparency_session {
827 let _ = kernel_handle_for_cb
828 .infra
829 .publish(KernelEvent::TokenUsageUpdate {
830 session_id: sid.clone(),
831 input_tokens: input_tokens as u64,
832 output_tokens: output_tokens as u64,
833 });
834 }
835 }
836 AgentEvent::Compaction {
837 event: CompactionEvent::Completed { result, .. },
838 } => {
839 handle_compaction(
840 result.summary.clone(),
841 session_id_for_callback.clone(),
842 memory_for_callback.clone(),
843 );
844 if let Some(ref sid) = transparency_session {
846 let _ =
847 kernel_handle_for_cb
848 .infra
849 .publish(KernelEvent::ReasoningFragment {
850 session_id: sid.clone(),
851 content: result.summary.clone(),
852 source: "compaction".to_string(),
853 });
854 }
855 }
856 _ => {}
857 }
858 })
859 .await;
860
861 let circuit = get_llm_circuit_breaker();
863 if result.is_err() {
864 circuit.record_failure();
865 crate::metrics::get_metrics()
866 .llm_circuit_breaker_state
867 .set(1.0);
868 } else {
869 circuit.record_success();
870 crate::metrics::get_metrics()
871 .llm_circuit_breaker_state
872 .set(0.0);
873 }
874
875 if let Err(e) = result {
876 tracing::error!(seed_id = %seed_id, error = %e, "Agent failed");
877 let s = exec_state.lock();
878 return Ok((
879 format!("Agent failed: {e}"),
880 s.steps_completed,
881 false,
882 s.trajectory_steps.clone(),
883 agent,
884 ));
885 }
886
887 let s = exec_state.lock();
888 tracing::info!(
889 seed_id = %seed_id,
890 steps = s.steps_completed,
891 success = s.success,
892 "Agent completed"
893 );
894
895 if !s.trajectory_steps.is_empty() {
898 if let Some(sona) = kernel_handle.agents.memory_manager().sona_engine() {
899 let steps = s.trajectory_steps.clone();
900 let success = s.success;
901 let sona = Arc::clone(sona);
902 let domain = infer_domain(&seed_goal);
903 tokio::spawn(async move {
904 let verdict = if success {
905 oxios_memory::memory::sona::Verdict::Success
906 } else {
907 oxios_memory::memory::sona::Verdict::Failure
908 };
909 let trajectory =
910 oxios_memory::memory::sona::Trajectory::new(steps, verdict, &domain);
911 if let Err(e) = sona.record(trajectory).await {
912 tracing::debug!(error = %e, "SONA trajectory recording failed (non-fatal)");
913 }
914 });
915 }
916 }
917
918 Ok((
919 s.final_content.clone(),
920 s.steps_completed,
921 s.success,
922 s.trajectory_steps.clone(),
923 agent,
924 ))
925}
926
927fn summarize_tool_result(result: &str, max_len: usize) -> String {
932 let trimmed = result.trim();
933 if trimmed.chars().count() <= max_len {
934 return trimmed.to_string();
935 }
936 let first_line = trimmed.lines().next().unwrap_or("");
938 if first_line.chars().count() <= max_len {
939 first_line.to_string()
940 } else {
941 let truncated: String = first_line.chars().take(max_len - 3).collect();
942 format!("{truncated}...")
943 }
944}
945
946fn infer_domain(goal: &str) -> String {
951 let lower = goal.to_lowercase();
952 let keywords: Vec<&str> = lower.split_whitespace().take(8).collect();
953
954 if keywords.iter().any(|k| {
956 [
957 "test",
958 "tests",
959 "spec",
960 "testing",
961 "assert",
962 "unit test",
963 "integration",
964 ]
965 .contains(k)
966 }) {
967 return "testing".to_string();
968 }
969 if keywords
970 .iter()
971 .any(|k| ["deploy", "release", "publish", "ship"].contains(k))
972 {
973 return "deployment".to_string();
974 }
975 if keywords
976 .iter()
977 .any(|k| ["fix", "bug", "patch", "repair", "debug"].contains(k))
978 {
979 return "bugfix".to_string();
980 }
981 if keywords
982 .iter()
983 .any(|k| ["refactor", "restructure", "reorganize", "rewrite"].contains(k))
984 {
985 return "refactoring".to_string();
986 }
987 if keywords
988 .iter()
989 .any(|k| ["doc", "document", "readme", "guide", "explain"].contains(k))
990 {
991 return "documentation".to_string();
992 }
993 if keywords
994 .iter()
995 .any(|k| ["build", "create", "implement", "add", "make", "new"].contains(k))
996 {
997 return "development".to_string();
998 }
999 if keywords
1000 .iter()
1001 .any(|k| ["analyze", "review", "audit", "inspect", "check"].contains(k))
1002 {
1003 return "analysis".to_string();
1004 }
1005 if keywords
1006 .iter()
1007 .any(|k| ["config", "setup", "install", "configure", "init"].contains(k))
1008 {
1009 return "configuration".to_string();
1010 }
1011
1012 let meaningful: Vec<&str> = lower
1014 .split_whitespace()
1015 .filter(|w| w.len() > 2)
1016 .take(2)
1017 .collect();
1018 if meaningful.len() >= 2 {
1019 meaningful.join("_")
1020 } else {
1021 "general".to_string()
1022 }
1023}
1024
1025fn handle_compaction(summary: String, session_id: String, memory_manager: Arc<MemoryManager>) {
1031 let entry = MemoryEntry {
1032 id: uuid::Uuid::new_v4().to_string(),
1033 memory_type: MemoryType::Conversation,
1034 tier: crate::memory::MemoryTier::Warm,
1035 content: summary,
1036 content_hash: 0,
1037 source: "compaction".to_string(),
1038 session_id: Some(session_id),
1039 tags: vec![],
1040 importance: 0.5,
1041 pinned: false,
1042 protection: crate::memory::ProtectionLevel::None,
1043 auto_classified: false,
1044 session_appearances: 0,
1045 user_corrected: false,
1046 seen_in_sessions: vec![],
1047 created_at: chrono::Utc::now(),
1048 accessed_at: chrono::Utc::now(),
1049 modified_at: chrono::Utc::now(),
1050 access_count: 0,
1051 decay_score: 1.0,
1052 compaction_level: 0,
1053 compacted_from: vec![],
1054 related_ids: vec![],
1055 contradicts: None,
1056 };
1057 tokio::spawn(async move {
1058 if let Err(e) = memory_manager.remember(entry).await {
1059 tracing::warn!(error = %e, "Failed to save compaction summary");
1060 }
1061 });
1062}
1063
1064fn build_system_prompt(
1070 seed: &Seed,
1071 persona_prompt: Option<&str>,
1072 capabilities_xml: Option<&str>,
1073 kernel_manifest: Option<&str>,
1074) -> String {
1075 let mut prompt = format!(
1076 "You are an autonomous agent in the Oxios operating system.\n\
1077 You execute Seeds — immutable specifications with goals, constraints, and\n\
1078 acceptance criteria. You have tools for reading, writing, editing files,\n\
1079 running commands, and accessing kernel services.\n\n\
1080 ## Goal\n\
1081 {}\n",
1082 seed.goal,
1083 );
1084
1085 if !seed.original_request.is_empty() && seed.original_request != seed.goal {
1088 prompt.push_str(&format!(
1089 "\n## User's Original Request\n{}\n",
1090 seed.original_request
1091 ));
1092 }
1093
1094 if !seed.constraints.is_empty() {
1095 prompt.push_str("\n## Constraints\n");
1096 for (i, c) in seed.constraints.iter().enumerate() {
1097 prompt.push_str(&format!("{}. {}\n", i + 1, c));
1098 }
1099 }
1100
1101 if !seed.acceptance_criteria.is_empty() {
1102 prompt.push_str("\n## Acceptance Criteria\n");
1103 for (i, c) in seed.acceptance_criteria.iter().enumerate() {
1104 prompt.push_str(&format!("{}. {}\n", i + 1, c));
1105 }
1106 }
1107
1108 if !seed.ontology.is_empty() {
1109 prompt.push_str("\n## Domain Entities\n");
1110 for e in &seed.ontology {
1111 prompt.push_str(&format!(
1112 "- **{}** ({}): {}\n",
1113 e.name, e.entity_type, e.description
1114 ));
1115 }
1116 }
1117
1118 if let Some(pp) = persona_prompt {
1120 prompt.push_str("\n## Persona\n");
1121 prompt.push_str(pp);
1122 prompt.push('\n');
1123 }
1124
1125 if let Some(xml) = capabilities_xml {
1127 prompt.push_str("\n## Available Capabilities\n");
1128 prompt.push_str("The following capabilities are relevant to your goal. ");
1129 prompt.push_str("Use the `read` tool to load SKILL.md for any program.\n\n");
1130 prompt.push_str(xml);
1131 prompt.push('\n');
1132 }
1133
1134 if let Some(manifest) = kernel_manifest {
1136 prompt.push('\n');
1137 prompt.push_str(manifest);
1138 prompt.push('\n');
1139 }
1140
1141 prompt.push_str(
1143 "\n## Execution Protocol\n\
1144 1. UNDERSTAND — Read the Seed completely before acting.\n\
1145 2. PLAN — Determine the minimal set of actions needed.\n\
1146 3. EXECUTE — Use tools to accomplish the goal. Prefer the simplest approach.\n\
1147 4. VERIFY — After each action, check the result: created a file? read it back.\n\
1148 5. REPORT — Summarize how each acceptance criterion was met, with evidence.\n\n\
1149 ## Hard Boundaries\n\
1150 - NEVER modify files outside the workspace scope\n\
1151 - NEVER execute destructive commands without confirming scope\n\
1152 - NEVER claim completion without evidence — show the output, not your opinion\n\
1153 - NEVER add features or improvements beyond the Seed scope\n\
1154 - If you cannot complete the Seed, say so and explain WHY\n\n\
1155 ## Scope Guard\n\
1156 The Seed defines your universe. Do not:\n\
1157 - Refactor code the Seed didn't mention\n\
1158 - Add tests the Seed didn't require\n\
1159 - Change configuration the Seed didn't specify\n\
1160 - \"Improve\" anything beyond what the acceptance criteria demand\n\n\
1161 ## Error Handling\n\
1162 - If a tool fails, read the error message carefully before retrying\n\
1163 - If a command fails, do NOT immediately retry with --force or sudo\n\
1164 - If stuck after 3 attempts, report the blocker rather than continuing to fail\n\n\
1165 ## Shape Matching\n\
1166 Match your output to the task: simple task → concise response.\n\
1167 Do not write 50 lines when 5 would do.\n\
1168 Use `exec` for all command execution (git, gh, osascript, etc.).",
1169 );
1170
1171 prompt
1172}
1173
1174fn build_user_prompt(seed: &Seed) -> String {
1176 format!(
1177 "Execute the following goal:\n\n{}\n\nAcceptance criteria:\n{}",
1178 seed.goal,
1179 seed.acceptance_criteria
1180 .iter()
1181 .enumerate()
1182 .map(|(i, c)| format!("{}. {}", i + 1, c))
1183 .collect::<Vec<_>>()
1184 .join("\n")
1185 )
1186}
1187
1188impl std::fmt::Debug for AgentRuntime {
1189 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1190 f.debug_struct("AgentRuntime")
1191 .field("model_id", &self.config.model_id)
1192 .finish()
1193 }
1194}
1195
1196#[cfg(test)]
1197mod tests {
1198 use super::*;
1199 use async_trait::async_trait;
1200 use oxi_sdk::{AgentTool, ToolContext, ToolError};
1201 use oxios_ouroboros::Entity;
1202 use serde_json::Value;
1203
1204 struct DummyTool {
1206 name: String,
1207 }
1208
1209 #[async_trait]
1210 impl AgentTool for DummyTool {
1211 fn name(&self) -> &str {
1212 &self.name
1213 }
1214 fn label(&self) -> &str {
1215 &self.name
1216 }
1217 fn description(&self) -> &str {
1218 "Test tool"
1219 }
1220 fn parameters_schema(&self) -> Value {
1221 serde_json::json!({"type": "object"})
1222 }
1223
1224 async fn execute(
1225 &self,
1226 _tool_call_id: &str,
1227 _params: Value,
1228 _shutdown: Option<tokio::sync::oneshot::Receiver<()>>,
1229 _ctx: &ToolContext,
1230 ) -> Result<oxi_sdk::AgentToolResult, ToolError> {
1231 Ok(oxi_sdk::AgentToolResult::success("ok"))
1232 }
1233 }
1234
1235 #[test]
1237 fn test_requires_tools_validation_passes() {
1238 let registry = ToolRegistry::new();
1239
1240 registry.register(DummyTool {
1241 name: "read".into(),
1242 });
1243 registry.register(DummyTool {
1244 name: "exec".into(),
1245 });
1246
1247 let missing = registry.missing(&["read", "exec"]);
1248
1249 assert!(
1250 missing.is_empty(),
1251 "Expected no missing tools, got: {:?}",
1252 missing
1253 );
1254 }
1255
1256 #[test]
1258 fn test_requires_tools_validation_fails() {
1259 let registry = ToolRegistry::new();
1260
1261 registry.register(DummyTool {
1262 name: "read".into(),
1263 });
1264
1265 let missing = registry.missing(&["read", "exec", "nonexistent"]);
1266
1267 assert_eq!(missing, vec!["exec", "nonexistent"]);
1268 }
1269
1270 #[test]
1271 fn test_build_system_prompt_includes_goal() {
1272 let seed = Seed {
1273 id: uuid::Uuid::new_v4(),
1274 goal: "Build a web server".into(),
1275 constraints: vec!["Must use Rust".into()],
1276 acceptance_criteria: vec!["Server responds to requests".into()],
1277 ontology: vec![Entity {
1278 name: "HttpServer".into(),
1279 entity_type: "struct".into(),
1280 description: "The main server struct".into(),
1281 }],
1282 created_at: chrono::Utc::now(),
1283 generation: 0,
1284 parent_seed_id: None,
1285 cspace_hint: None,
1286 original_request: String::new(),
1287 output_schema: None,
1288 };
1289
1290 let prompt = build_system_prompt(&seed, None, None, None);
1291
1292 assert!(prompt.contains("Build a web server"));
1293 assert!(prompt.contains("Must use Rust"));
1294 assert!(prompt.contains("Server responds to requests"));
1295 assert!(prompt.contains("HttpServer"));
1296 assert!(prompt.contains("struct"));
1297 }
1298
1299 #[test]
1300 fn test_build_system_prompt_empty() {
1301 let seed = Seed {
1302 id: uuid::Uuid::new_v4(),
1303 goal: "Test goal".into(),
1304 constraints: vec![],
1305 acceptance_criteria: vec![],
1306 ontology: vec![],
1307 created_at: chrono::Utc::now(),
1308 generation: 0,
1309 parent_seed_id: None,
1310 cspace_hint: None,
1311 original_request: String::new(),
1312 output_schema: None,
1313 };
1314
1315 let prompt = build_system_prompt(&seed, None, None, None);
1316
1317 assert!(prompt.contains("Test goal"));
1318 }
1319
1320 #[test]
1321 fn test_infer_domain_testing() {
1322 assert_eq!(infer_domain("run all unit tests for the kernel"), "testing");
1323 }
1324
1325 #[test]
1326 fn test_infer_domain_deployment() {
1327 assert_eq!(
1328 infer_domain("deploy the web service to production"),
1329 "deployment"
1330 );
1331 }
1332
1333 #[test]
1334 fn test_infer_domain_bugfix() {
1335 assert_eq!(infer_domain("fix the null pointer error in main"), "bugfix");
1336 }
1337
1338 #[test]
1339 fn test_infer_domain_development() {
1340 assert_eq!(
1341 infer_domain("create a new REST API endpoint"),
1342 "development"
1343 );
1344 }
1345
1346 #[test]
1347 fn test_infer_domain_analysis() {
1348 assert_eq!(
1349 infer_domain("review the code for security issues"),
1350 "analysis"
1351 );
1352 }
1353
1354 #[test]
1355 fn test_infer_domain_fallback() {
1356 let domain = infer_domain("optimize performance metrics");
1357 assert!(!domain.is_empty());
1359 }
1360}