use anyhow::Result;
use oxi_sdk::{
Agent, AgentConfig, AgentEvent, CompactionEvent, CompactionStrategy, ProviderResolver,
};
use oxi_sdk::{SearchCache, ToolExecutionMode, ToolRegistry};
use parking_lot::Mutex;
use std::sync::Arc;
use crate::access_manager::{AccessGate, AgentContext, TracingAuditSink, TrailAuditSink};
use crate::audit_trail::AuditTrail;
use crate::capability::resolve::resolve_cspace;
use crate::engine::OxiosEngine;
use crate::memory::{MemoryEntry, MemoryManager, MemoryType};
use crate::persona_manager::PersonaManager;
use crate::tools::registration::register_tools_from_cspace_gated;
use crate::session_context::SessionContext;
use crate::types::AgentId;
use crate::KernelHandle;
use oxios_ouroboros::{ExecutionResult, Seed};
static LLM_CIRCUIT_BREAKER: std::sync::OnceLock<oxi_sdk::ProviderCircuitBreaker> =
std::sync::OnceLock::new();
fn get_llm_circuit_breaker() -> &'static oxi_sdk::ProviderCircuitBreaker {
LLM_CIRCUIT_BREAKER.get_or_init(|| {
oxi_sdk::ProviderCircuitBreaker::new(
"global".to_string(),
oxi_sdk::CircuitBreakerConfig::default(),
)
})
}
#[derive(Debug, Clone)]
pub struct AgentRuntimeConfig {
pub model_id: String,
pub max_iterations: usize,
pub tool_execution: ToolExecutionMode,
pub auto_retry_enabled: bool,
pub project_paths: Vec<std::path::PathBuf>,
pub workspace_dir: Option<std::path::PathBuf>,
pub api_key: Option<String>,
pub provider_options: Option<oxi_sdk::ProviderOptions>,
pub rate_limit_per_minute: usize,
pub token_budget: usize,
pub audit_tool_calls: bool,
pub provider_rpm: u32,
}
impl Default for AgentRuntimeConfig {
fn default() -> Self {
Self {
model_id: String::new(),
max_iterations: 8,
tool_execution: ToolExecutionMode::Parallel,
auto_retry_enabled: true,
project_paths: Vec::new(),
workspace_dir: None,
api_key: None,
provider_options: None,
rate_limit_per_minute: 0,
token_budget: 0,
audit_tool_calls: false,
provider_rpm: 0,
}
}
}
#[derive(Default)]
struct ExecuteState {
final_content: String,
steps_completed: usize,
success: bool,
trajectory_steps: Vec<crate::memory::sona::TrajectoryStep>,
pending_tools: std::collections::HashMap<String, (std::time::Instant, usize)>,
}
pub struct AgentRuntime {
engine: Arc<OxiosEngine>,
config: AgentRuntimeConfig,
kernel_handle: Arc<KernelHandle>,
persona_manager: Option<Arc<PersonaManager>>,
tool_retriever: Option<Arc<crate::tools::retrieval::ToolRetriever>>,
routing_stats: Option<Arc<crate::kernel_handle::RoutingStats>>,
}
impl AgentRuntime {
pub fn new(
engine: Arc<OxiosEngine>,
model_id: impl Into<String>,
kernel_handle: Arc<KernelHandle>,
routing_stats: Option<Arc<crate::kernel_handle::RoutingStats>>,
) -> Self {
Self {
engine,
config: AgentRuntimeConfig {
model_id: model_id.into(),
..Default::default()
},
kernel_handle,
persona_manager: None,
tool_retriever: None,
routing_stats,
}
}
pub fn with_persona_manager(mut self, pm: Arc<PersonaManager>) -> Self {
self.persona_manager = Some(pm);
self
}
pub fn with_config(mut self, config: AgentRuntimeConfig) -> Self {
self.config = config;
self
}
pub fn with_tool_retriever(
mut self,
retriever: Arc<crate::tools::retrieval::ToolRetriever>,
) -> Self {
self.tool_retriever = Some(retriever);
self
}
pub async fn execute(
&self,
agent_id: AgentId,
seed: &Seed,
session_ctx: &mut SessionContext,
) -> Result<ExecutionResult> {
let prompt = build_user_prompt(seed);
let persona_prompt = self
.persona_manager
.as_ref()
.map(|pm| pm.active_system_prompt())
.filter(|s| !s.trim().is_empty());
let persona_role = self
.persona_manager
.as_ref()
.and_then(|pm| pm.get_active_persona().map(|p| p.role.clone()));
let cspace = resolve_cspace(
seed.cspace_hint.as_deref(),
persona_role.as_deref(),
Some("worker"),
agent_id,
);
let mut system_prompt = build_system_prompt(seed, persona_prompt.as_deref(), None, None);
let capabilities_xml = if let Some(ref retriever) = self.tool_retriever {
match retriever.embedder().embed(&seed.goal).await {
Ok(query_vec) => {
let results = retriever.retrieve(&query_vec, 8);
if results.is_empty() {
None
} else {
let xml = crate::tools::retrieval::format_capability_index(&results);
tracing::info!(count = results.len(), "Retrieved relevant capabilities");
Some(xml)
}
}
Err(e) => {
tracing::warn!(error = %e, "Failed to embed seed goal for retrieval");
None
}
}
} else {
None
};
let kernel_manifest = {
let domains = cspace.active_domains();
if domains.is_empty() {
None
} else {
Some(crate::tools::retrieval::build_kernel_manifest(&domains))
}
};
if capabilities_xml.is_some() || kernel_manifest.is_some() {
system_prompt = build_system_prompt(
seed,
persona_prompt.as_deref(),
capabilities_xml.as_deref(),
kernel_manifest.as_deref(),
);
}
let memory_manager = self.kernel_handle.agents.memory_manager();
match memory_manager
.recall_with_proactive(&seed.goal, &mut session_ctx.recall_timing)
.await
{
Ok(memories) if !memories.is_empty() => {
tracing::info!(count = memories.len(), "Recalled memories for seed");
system_prompt = memory_manager.blend_into_prompt(&memories, &system_prompt);
}
Ok(_) => tracing::debug!("No memories recalled"),
Err(e) => tracing::warn!(error = %e, "Failed to recall memories"),
}
if let Some(sona) = memory_manager.sona_engine() {
match sona.adapt(&seed.goal).await {
Ok(Some(pattern)) if pattern.confidence > 0.5 => {
tracing::info!(
domain = %pattern.domain,
confidence = pattern.confidence,
"SONA learned pattern injected"
);
system_prompt.push_str(&format!(
"\n\n## Learned Strategy (confidence: {:.0}%)\n{}\n",
pattern.confidence * 100.0,
pattern.strategy,
));
}
Ok(_) => tracing::debug!("No high-confidence SONA pattern found"),
Err(e) => tracing::debug!(error = %e, "SONA adapt failed (non-fatal)"),
}
}
match self
.kernel_handle
.knowledge_lens
.recall_for_context(&seed.goal, 5)
.await
{
Ok(ctx) if !ctx.notes.is_empty() => {
tracing::info!(
notes = ctx.notes.len(),
memories = ctx.memories.len(),
"Recalled knowledge context for seed"
);
let knowledge_blend = ctx
.notes
.iter()
.take(3)
.map(|n| format!("## {}\n\n{}", n.name, n.content))
.collect::<Vec<_>>()
.join("\n\n");
system_prompt.push_str("\n\n## Relevant Knowledge\n\n");
system_prompt.push_str(&knowledge_blend);
}
Ok(_) => tracing::debug!("No knowledge recalled"),
Err(e) => tracing::warn!(error = %e, "Failed to recall knowledge context"),
}
let _model = self.engine.resolve_model(&self.config.model_id)?;
let seed_id = seed.id;
let config = self.config.clone();
let kernel_handle = Arc::clone(&self.kernel_handle);
let audit_trail: Option<Arc<AuditTrail>> =
Some(Arc::clone(&self.kernel_handle.security.audit_trail));
let (final_content, steps_completed, success, _agent) = {
run_agent(
&config,
&self.engine,
kernel_handle,
system_prompt,
prompt,
seed_id,
seed.goal.clone(),
agent_id,
cspace,
audit_trail,
self.routing_stats.clone(),
)
.await?
};
tracing::info!(
seed_id = %seed_id,
steps = steps_completed,
success,
"AgentRuntime finished"
);
Ok(ExecutionResult {
output: if final_content.is_empty() {
"Agent execution completed".into()
} else {
final_content
},
steps_completed,
success,
})
}
}
#[allow(clippy::too_many_arguments)]
async fn run_agent(
config: &AgentRuntimeConfig,
engine: &OxiosEngine,
kernel_handle: Arc<KernelHandle>,
system_prompt: String,
prompt: String,
seed_id: uuid::Uuid,
seed_goal: String,
agent_id: AgentId,
cspace: crate::capability::CSpace,
audit_trail: Option<Arc<AuditTrail>>,
routing_stats: Option<Arc<crate::kernel_handle::RoutingStats>>,
) -> Result<(String, usize, bool, Arc<Agent>)> {
let workspace = if !config.project_paths.is_empty() {
config.project_paths[0].clone()
} else if let Some(ref ws) = config.workspace_dir {
ws.clone()
} else {
std::env::temp_dir()
.join("oxios-agent-workspace")
.join(agent_id.to_string())
};
let _ = std::fs::create_dir_all(&workspace);
tracing::debug!(workspace = %workspace.display(), "Agent workspace scoped");
let _trace_guard = crate::observability::tracer().start(
format!("seed-{}", &seed_id.to_string()[..8]).as_str(),
oxi_sdk::SpanKind::Agent,
);
let registry = ToolRegistry::new();
let search_cache = Arc::new(SearchCache::new());
let agent_context = AgentContext {
agent_id,
agent_name: format!("agent-{agent_id}"),
cspace: Arc::new(cspace.clone()),
};
let audit_sink: Arc<dyn crate::access_manager::AuditSink> = if let Some(trail) = audit_trail {
let audit_path = kernel_handle
.state
.workspace_path()
.join("audit")
.join("access.jsonl");
Arc::new(TrailAuditSink::new(trail, audit_path))
} else {
Arc::new(TracingAuditSink)
};
let access_gate = Arc::new(AccessGate::new(
kernel_handle.exec.access_manager().clone(),
Arc::new(kernel_handle.exec.config().clone()),
audit_sink,
));
register_tools_from_cspace_gated(
®istry,
&kernel_handle,
&cspace,
search_cache,
agent_id,
access_gate,
agent_context,
);
tracing::info!(
seed_id = %seed_id,
capabilities = cspace.len(),
"Tools registered from CSpace"
);
let agent_config = AgentConfig {
name: format!("agent-{agent_id}"),
description: None,
model_id: config.model_id.clone(),
system_prompt: Some(system_prompt),
max_iterations: config.max_iterations,
timeout_seconds: 300,
temperature: Some(0.7),
max_tokens: Some(8192),
compaction_strategy: CompactionStrategy::Threshold(0.8),
compaction_instruction: None,
context_window: 128_000,
api_key: config.api_key.clone(),
workspace_dir: config.project_paths.first().cloned(),
output_mode: None,
provider_options: config.provider_options.clone(),
};
let resolver: Arc<dyn ProviderResolver> = Arc::new(engine.oxi().clone());
let provider_name = engine.resolve_model(&config.model_id)?.provider;
let provider = if config.provider_rpm > 0 {
engine.pooled_provider(&provider_name, config.provider_rpm)?
} else {
engine.create_provider(&provider_name)?
};
let mut pipeline = oxi_sdk::MiddlewarePipeline::new();
if config.rate_limit_per_minute > 0 {
pipeline = pipeline.push(oxi_sdk::middleware::builtins::RateLimitMiddleware::new(
config.rate_limit_per_minute,
));
}
if config.token_budget > 0 {
pipeline = pipeline.push(oxi_sdk::middleware::builtins::TokenBudgetMiddleware::new(
config.token_budget,
));
}
if config.audit_tool_calls {
pipeline = pipeline.push(oxi_sdk::middleware::builtins::LoggingMiddleware::new(
tracing::Level::INFO,
));
}
let agent = Arc::new(Agent::new_with_resolver(
provider,
agent_config,
Arc::new(registry),
resolver,
));
if !pipeline.is_empty() {
let terminate_flag = Arc::new(std::sync::atomic::AtomicBool::new(false));
let agent_id_for_hooks = agent_id.to_string();
let hooks = oxi_sdk::middleware::build_hooks(
Arc::new(pipeline),
agent_id_for_hooks,
terminate_flag,
);
agent.set_hooks(hooks);
}
let exec_state = Arc::new(Mutex::new(ExecuteState::default()));
let exec_state_cb = Arc::clone(&exec_state);
let memory_for_callback: Arc<MemoryManager> = (*kernel_handle.agents.memory_manager()).clone();
let session_id_for_callback = seed_id.to_string();
let model_id_for_callback = config.model_id.clone();
let agent_id_for_callback = agent_id.to_string();
let routing_stats_for_cb = routing_stats.clone();
let result = agent
.run_streaming(prompt, move |event| {
let mut s = exec_state_cb.lock();
match event {
AgentEvent::ToolExecutionStart {
tool_name,
tool_call_id,
..
} => {
let idx = s.trajectory_steps.len();
s.pending_tools
.insert(tool_call_id.clone(), (std::time::Instant::now(), idx));
s.trajectory_steps
.push(crate::memory::sona::TrajectoryStep {
input: tool_name.clone(),
output: String::new(),
duration_ms: 0,
confidence: 0.0,
});
}
AgentEvent::ToolExecutionEnd {
tool_call_id,
is_error,
result,
..
} => {
if !is_error {
s.steps_completed += 1;
}
if let Some((start, idx)) = s.pending_tools.remove(tool_call_id.as_str()) {
let duration_ms = start.elapsed().as_millis() as u64;
if let Some(step) = s.trajectory_steps.get_mut(idx) {
step.output = summarize_tool_result(&result.content, 200);
step.duration_ms = duration_ms;
step.confidence = if is_error { 0.3 } else { 0.8 };
}
}
}
AgentEvent::AgentEnd {
messages,
stop_reason,
..
} => {
if let Some(oxi_sdk::Message::Assistant(a)) = messages.last() {
s.final_content = a.text_content();
}
s.success = stop_reason.as_deref() == Some("Stop");
}
AgentEvent::Error { message, .. } => {
s.final_content = message.clone();
s.success = false;
}
AgentEvent::Usage {
input_tokens,
output_tokens,
} => {
let agent_label = format!("agent-{agent_id_for_callback}");
crate::observability::cost_tracker().record(
&agent_label,
&oxi_sdk::Model::new(
&model_id_for_callback,
&model_id_for_callback,
oxi_sdk::Api::OpenAiCompletions,
"unknown",
"https://unknown.com",
),
oxi_sdk::TokenUsage {
input: input_tokens as u64,
output: output_tokens as u64,
cache_read: 0,
cache_write: 0,
},
);
if let Some(stats) = &routing_stats_for_cb {
let cost = crate::kernel_handle::engine_api::estimate_cost(
&model_id_for_callback,
input_tokens as u64,
output_tokens as u64,
);
stats.record_model_usage(&model_id_for_callback, cost);
}
}
AgentEvent::Compaction {
event: CompactionEvent::Completed { result, .. },
} => {
handle_compaction(
result.summary.clone(),
session_id_for_callback.clone(),
memory_for_callback.clone(),
);
}
_ => {}
}
})
.await;
let circuit = get_llm_circuit_breaker();
if result.is_err() {
circuit.record_failure();
crate::metrics::get_metrics()
.llm_circuit_breaker_state
.set(1.0);
} else {
circuit.record_success();
crate::metrics::get_metrics()
.llm_circuit_breaker_state
.set(0.0);
}
if let Err(e) = result {
tracing::error!(seed_id = %seed_id, error = %e, "Agent failed");
let s = exec_state.lock();
return Ok((
format!("Agent failed: {e}"),
s.steps_completed,
false,
agent,
));
}
let s = exec_state.lock();
tracing::info!(
seed_id = %seed_id,
steps = s.steps_completed,
success = s.success,
"Agent completed"
);
if !s.trajectory_steps.is_empty() {
if let Some(sona) = kernel_handle.agents.memory_manager().sona_engine() {
let steps = s.trajectory_steps.clone();
let success = s.success;
let sona = Arc::clone(sona);
let domain = infer_domain(&seed_goal);
tokio::spawn(async move {
let verdict = if success {
crate::memory::sona::Verdict::Success
} else {
crate::memory::sona::Verdict::Failure
};
let trajectory = crate::memory::sona::Trajectory::new(steps, verdict, &domain);
if let Err(e) = sona.record(trajectory).await {
tracing::debug!(error = %e, "SONA trajectory recording failed (non-fatal)");
}
});
}
}
Ok((s.final_content.clone(), s.steps_completed, s.success, agent))
}
fn summarize_tool_result(result: &str, max_len: usize) -> String {
let trimmed = result.trim();
if trimmed.chars().count() <= max_len {
return trimmed.to_string();
}
let first_line = trimmed.lines().next().unwrap_or("");
if first_line.chars().count() <= max_len {
first_line.to_string()
} else {
let truncated: String = first_line.chars().take(max_len - 3).collect();
format!("{truncated}...")
}
}
fn infer_domain(goal: &str) -> String {
let lower = goal.to_lowercase();
let keywords: Vec<&str> = lower.split_whitespace().take(8).collect();
if keywords.iter().any(|k| {
[
"test",
"tests",
"spec",
"testing",
"assert",
"unit test",
"integration",
]
.contains(k)
}) {
return "testing".to_string();
}
if keywords
.iter()
.any(|k| ["deploy", "release", "publish", "ship"].contains(k))
{
return "deployment".to_string();
}
if keywords
.iter()
.any(|k| ["fix", "bug", "patch", "repair", "debug"].contains(k))
{
return "bugfix".to_string();
}
if keywords
.iter()
.any(|k| ["refactor", "restructure", "reorganize", "rewrite"].contains(k))
{
return "refactoring".to_string();
}
if keywords
.iter()
.any(|k| ["doc", "document", "readme", "guide", "explain"].contains(k))
{
return "documentation".to_string();
}
if keywords
.iter()
.any(|k| ["build", "create", "implement", "add", "make", "new"].contains(k))
{
return "development".to_string();
}
if keywords
.iter()
.any(|k| ["analyze", "review", "audit", "inspect", "check"].contains(k))
{
return "analysis".to_string();
}
if keywords
.iter()
.any(|k| ["config", "setup", "install", "configure", "init"].contains(k))
{
return "configuration".to_string();
}
let meaningful: Vec<&str> = lower
.split_whitespace()
.filter(|w| w.len() > 2)
.take(2)
.collect();
if meaningful.len() >= 2 {
meaningful.join("_")
} else {
"general".to_string()
}
}
fn handle_compaction(summary: String, session_id: String, memory_manager: Arc<MemoryManager>) {
let entry = MemoryEntry {
id: uuid::Uuid::new_v4().to_string(),
memory_type: MemoryType::Conversation,
tier: crate::memory::MemoryTier::Warm,
content: summary,
content_hash: 0,
source: "compaction".to_string(),
session_id: Some(session_id),
tags: vec![],
importance: 0.5,
pinned: false,
protection: crate::memory::ProtectionLevel::None,
auto_classified: false,
session_appearances: 0,
user_corrected: false,
seen_in_sessions: vec![],
created_at: chrono::Utc::now(),
accessed_at: chrono::Utc::now(),
modified_at: chrono::Utc::now(),
access_count: 0,
decay_score: 1.0,
compaction_level: 0,
compacted_from: vec![],
related_ids: vec![],
contradicts: None,
};
tokio::spawn(async move {
if let Err(e) = memory_manager.remember(entry).await {
tracing::warn!(error = %e, "Failed to save compaction summary");
}
});
}
fn build_system_prompt(
seed: &Seed,
persona_prompt: Option<&str>,
capabilities_xml: Option<&str>,
kernel_manifest: Option<&str>,
) -> String {
let mut prompt = format!(
"You are an autonomous agent in the Oxios operating system.\n\
You execute Seeds — immutable specifications with goals, constraints, and\n\
acceptance criteria. You have tools for reading, writing, editing files,\n\
running commands, and accessing kernel services.\n\n\
## Goal\n\
{}\n",
seed.goal,
);
if !seed.original_request.is_empty() && seed.original_request != seed.goal {
prompt.push_str(&format!(
"\n## User's Original Request\n{}\n",
seed.original_request
));
}
if !seed.constraints.is_empty() {
prompt.push_str("\n## Constraints\n");
for (i, c) in seed.constraints.iter().enumerate() {
prompt.push_str(&format!("{}. {}\n", i + 1, c));
}
}
if !seed.acceptance_criteria.is_empty() {
prompt.push_str("\n## Acceptance Criteria\n");
for (i, c) in seed.acceptance_criteria.iter().enumerate() {
prompt.push_str(&format!("{}. {}\n", i + 1, c));
}
}
if !seed.ontology.is_empty() {
prompt.push_str("\n## Domain Entities\n");
for e in &seed.ontology {
prompt.push_str(&format!(
"- **{}** ({}): {}\n",
e.name, e.entity_type, e.description
));
}
}
if let Some(pp) = persona_prompt {
prompt.push_str("\n## Persona\n");
prompt.push_str(pp);
prompt.push('\n');
}
if let Some(xml) = capabilities_xml {
prompt.push_str("\n## Available Capabilities\n");
prompt.push_str("The following capabilities are relevant to your goal. ");
prompt.push_str("Use the `read` tool to load SKILL.md for any program.\n\n");
prompt.push_str(xml);
prompt.push('\n');
}
if let Some(manifest) = kernel_manifest {
prompt.push('\n');
prompt.push_str(manifest);
prompt.push('\n');
}
prompt.push_str(
"\n## Execution Protocol\n\
1. UNDERSTAND — Read the Seed completely before acting.\n\
2. PLAN — Determine the minimal set of actions needed.\n\
3. EXECUTE — Use tools to accomplish the goal. Prefer the simplest approach.\n\
4. VERIFY — After each action, check the result: created a file? read it back.\n\
5. REPORT — Summarize how each acceptance criterion was met, with evidence.\n\n\
## Hard Boundaries\n\
- NEVER modify files outside the workspace scope\n\
- NEVER execute destructive commands without confirming scope\n\
- NEVER claim completion without evidence — show the output, not your opinion\n\
- NEVER add features or improvements beyond the Seed scope\n\
- If you cannot complete the Seed, say so and explain WHY\n\n\
## Scope Guard\n\
The Seed defines your universe. Do not:\n\
- Refactor code the Seed didn't mention\n\
- Add tests the Seed didn't require\n\
- Change configuration the Seed didn't specify\n\
- \"Improve\" anything beyond what the acceptance criteria demand\n\n\
## Error Handling\n\
- If a tool fails, read the error message carefully before retrying\n\
- If a command fails, do NOT immediately retry with --force or sudo\n\
- If stuck after 3 attempts, report the blocker rather than continuing to fail\n\n\
## Shape Matching\n\
Match your output to the task: simple task → concise response.\n\
Do not write 50 lines when 5 would do.\n\
Use `exec` for all command execution (git, gh, osascript, etc.).",
);
prompt
}
fn build_user_prompt(seed: &Seed) -> String {
format!(
"Execute the following goal:\n\n{}\n\nAcceptance criteria:\n{}",
seed.goal,
seed.acceptance_criteria
.iter()
.enumerate()
.map(|(i, c)| format!("{}. {}", i + 1, c))
.collect::<Vec<_>>()
.join("\n")
)
}
impl std::fmt::Debug for AgentRuntime {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("AgentRuntime")
.field("model_id", &self.config.model_id)
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
use async_trait::async_trait;
use oxi_sdk::{AgentTool, ToolContext, ToolError};
use oxios_ouroboros::Entity;
use serde_json::Value;
struct DummyTool {
name: String,
}
#[async_trait]
impl AgentTool for DummyTool {
fn name(&self) -> &str {
&self.name
}
fn label(&self) -> &str {
&self.name
}
fn description(&self) -> &str {
"Test tool"
}
fn parameters_schema(&self) -> Value {
serde_json::json!({"type": "object"})
}
async fn execute(
&self,
_tool_call_id: &str,
_params: Value,
_shutdown: Option<tokio::sync::oneshot::Receiver<()>>,
_ctx: &ToolContext,
) -> Result<oxi_sdk::AgentToolResult, ToolError> {
Ok(oxi_sdk::AgentToolResult::success("ok"))
}
}
#[test]
fn test_requires_tools_validation_passes() {
let registry = ToolRegistry::new();
registry.register(DummyTool {
name: "read".into(),
});
registry.register(DummyTool {
name: "exec".into(),
});
let missing = registry.missing(&["read", "exec"]);
assert!(
missing.is_empty(),
"Expected no missing tools, got: {:?}",
missing
);
}
#[test]
fn test_requires_tools_validation_fails() {
let registry = ToolRegistry::new();
registry.register(DummyTool {
name: "read".into(),
});
let missing = registry.missing(&["read", "exec", "nonexistent"]);
assert_eq!(missing, vec!["exec", "nonexistent"]);
}
#[test]
fn test_build_system_prompt_includes_goal() {
let seed = Seed {
id: uuid::Uuid::new_v4(),
goal: "Build a web server".into(),
constraints: vec!["Must use Rust".into()],
acceptance_criteria: vec!["Server responds to requests".into()],
ontology: vec![Entity {
name: "HttpServer".into(),
entity_type: "struct".into(),
description: "The main server struct".into(),
}],
created_at: chrono::Utc::now(),
generation: 0,
parent_seed_id: None,
cspace_hint: None,
original_request: String::new(),
output_schema: None,
};
let prompt = build_system_prompt(&seed, None, None, None);
assert!(prompt.contains("Build a web server"));
assert!(prompt.contains("Must use Rust"));
assert!(prompt.contains("Server responds to requests"));
assert!(prompt.contains("HttpServer"));
assert!(prompt.contains("struct"));
}
#[test]
fn test_build_system_prompt_empty() {
let seed = Seed {
id: uuid::Uuid::new_v4(),
goal: "Test goal".into(),
constraints: vec![],
acceptance_criteria: vec![],
ontology: vec![],
created_at: chrono::Utc::now(),
generation: 0,
parent_seed_id: None,
cspace_hint: None,
original_request: String::new(),
output_schema: None,
};
let prompt = build_system_prompt(&seed, None, None, None);
assert!(prompt.contains("Test goal"));
}
#[test]
fn test_infer_domain_testing() {
assert_eq!(infer_domain("run all unit tests for the kernel"), "testing");
}
#[test]
fn test_infer_domain_deployment() {
assert_eq!(
infer_domain("deploy the web service to production"),
"deployment"
);
}
#[test]
fn test_infer_domain_bugfix() {
assert_eq!(infer_domain("fix the null pointer error in main"), "bugfix");
}
#[test]
fn test_infer_domain_development() {
assert_eq!(
infer_domain("create a new REST API endpoint"),
"development"
);
}
#[test]
fn test_infer_domain_analysis() {
assert_eq!(
infer_domain("review the code for security issues"),
"analysis"
);
}
#[test]
fn test_infer_domain_fallback() {
let domain = infer_domain("optimize performance metrics");
assert!(!domain.is_empty());
}
}