use std::sync::{Arc, Mutex};
use std::time::Instant;
use crate::classifier;
use crate::executor::{self, AccumulatedContext};
use crate::handle::ReasoningInferenceHandle;
use crate::selector;
use crate::types::*;
use crate::verifier;
use crate::ReasonError;
use car_memgine::graph::SkillOutcome;
use car_memgine::MemgineEngine;
use chrono::Utc;
#[derive(Debug, Clone)]
pub enum ReasoningEvent {
Classified { class: ProblemClass },
PlanSelected { actions: Vec<ActionKind> },
ActionStarted { action: ActionKind },
ActionCompleted { outcome: ActionOutcome },
Complete { result: ReasoningResult },
}
pub struct ReasoningSession {
memgine: Arc<Mutex<MemgineEngine>>,
inference: Arc<dyn ReasoningInferenceHandle>,
}
impl ReasoningSession {
pub fn new(
memgine: Arc<Mutex<MemgineEngine>>,
inference: Arc<dyn ReasoningInferenceHandle>,
) -> Self {
Self { memgine, inference }
}
pub async fn reason_streaming<F>(
&self,
problem: &str,
mut on_event: F,
) -> Result<ReasoningResult, ReasonError>
where
F: FnMut(&ReasoningEvent),
{
let session_start = Instant::now();
let session_id = uuid::Uuid::new_v4().to_string();
let memory_context = {
let mut engine = self
.memgine
.lock()
.map_err(|_| ReasonError::SessionError("lock poisoned".into()))?;
engine.build_context(problem)
};
let problem_class = classifier::classify_problem(problem).await?;
on_event(&ReasoningEvent::Classified {
class: problem_class,
});
let actions = {
let engine = self
.memgine
.lock()
.map_err(|_| ReasonError::SessionError("lock poisoned".into()))?;
selector::select_actions(&engine, problem_class, problem)
};
on_event(&ReasoningEvent::PlanSelected {
actions: actions.iter().map(|(k, _)| *k).collect(),
});
let mut outcomes: Vec<ActionOutcome> = Vec::new();
let mut ctx = AccumulatedContext::new(problem, &memory_context, problem_class);
for (action_kind, action_config) in &actions {
on_event(&ReasoningEvent::ActionStarted {
action: *action_kind,
});
let outcome = executor::execute_action(
self.inference.as_ref(),
*action_kind,
action_config,
&ctx,
)
.await?;
on_event(&ReasoningEvent::ActionCompleted {
outcome: outcome.clone(),
});
if outcome.action == ActionKind::Locate {
if let Some(split) = outcome.output.find("---SOURCE_CODE_START---") {
ctx.locations = outcome.output[..split].trim().to_string();
ctx.source_code = outcome.output[split + 23..].trim().to_string();
} else {
ctx.locations = outcome.output.clone();
}
} else {
ctx.integrate(&outcome);
}
outcomes.push(outcome);
}
{
let mut engine = self
.memgine
.lock()
.map_err(|_| ReasonError::SessionError("lock poisoned".into()))?;
for outcome in &outcomes {
let skill_name = outcome.action.skill_name();
let skill_outcome = if outcome.success {
SkillOutcome::Success
} else {
SkillOutcome::Fail
};
engine.report_outcome(&skill_name, skill_outcome);
}
}
{
let action_results: Vec<(String, bool, f64, String)> = outcomes
.iter()
.map(|o| {
(
o.trace_id.clone(),
o.success,
o.confidence,
o.output.clone(),
)
})
.collect();
let _ = self
.inference
.record_inferred_outcomes(action_results)
.await;
}
let total_latency = session_start.elapsed().as_millis() as u64;
let mut suggestions = extract_suggestions(&ctx);
for s in &mut suggestions {
verifier::verify_suggestion(s);
}
let overall_confidence = if outcomes.is_empty() {
0.0
} else {
outcomes.iter().map(|o| o.confidence).sum::<f64>() / outcomes.len() as f64
};
let result = ReasoningResult {
session_id,
problem_class,
diagnosis: ctx.diagnosis.clone(),
suggestions,
explanation: ctx.explanation.clone(),
actions_taken: outcomes,
overall_confidence,
total_latency_ms: total_latency,
};
on_event(&ReasoningEvent::Complete {
result: result.clone(),
});
self.store_session_fact(problem, &result);
Ok(result)
}
pub async fn reason(&self, problem: &str) -> Result<ReasoningResult, ReasonError> {
self.reason_streaming(problem, |_| {}).await
}
pub async fn reason_with_context(
&self,
problem: &str,
source_code: &str,
) -> Result<ReasoningResult, ReasonError> {
let session_start = Instant::now();
let session_id = uuid::Uuid::new_v4().to_string();
let memory_context = {
let engine = self
.memgine
.lock()
.map_err(|_| ReasonError::SessionError("lock poisoned".into()))?;
let fact_count = engine.valid_fact_count();
if fact_count > 0 {
format!("[{} prior facts available]", fact_count)
} else {
String::new()
}
};
let problem_class = classifier::classify_problem(problem).await?;
let actions = {
let engine = self
.memgine
.lock()
.map_err(|_| ReasonError::SessionError("lock poisoned".into()))?;
selector::select_actions(&engine, problem_class, problem)
};
let mut outcomes: Vec<ActionOutcome> = Vec::new();
let mut ctx = AccumulatedContext::new(problem, &memory_context, problem_class);
ctx.source_code = source_code.to_string();
ctx.locations = format!("Pre-provided source context ({} bytes)", source_code.len());
for (action_kind, action_config) in &actions {
if *action_kind == ActionKind::Locate {
outcomes.push(ActionOutcome {
action: ActionKind::Locate,
model_used: "pre-provided".into(),
trace_id: String::new(),
latency_ms: 0,
output: ctx.locations.clone(),
confidence: 1.0,
success: true,
});
continue;
}
let outcome = executor::execute_action(
self.inference.as_ref(),
*action_kind,
action_config,
&ctx,
)
.await?;
ctx.integrate(&outcome);
outcomes.push(outcome);
}
{
let mut engine = self
.memgine
.lock()
.map_err(|_| ReasonError::SessionError("lock poisoned".into()))?;
for outcome in &outcomes {
let skill_name = outcome.action.skill_name();
let skill_outcome = if outcome.success {
SkillOutcome::Success
} else {
SkillOutcome::Fail
};
engine.report_outcome(&skill_name, skill_outcome);
}
}
{
let action_results: Vec<(String, bool, f64, String)> = outcomes
.iter()
.map(|o| {
(
o.trace_id.clone(),
o.success,
o.confidence,
o.output.clone(),
)
})
.collect();
let _ = self
.inference
.record_inferred_outcomes(action_results)
.await;
}
let total_latency = session_start.elapsed().as_millis() as u64;
let mut suggestions = extract_suggestions(&ctx);
for s in &mut suggestions {
verifier::verify_suggestion(s);
}
let overall_confidence = if outcomes.is_empty() {
0.0
} else {
outcomes.iter().map(|o| o.confidence).sum::<f64>() / outcomes.len() as f64
};
let result = ReasoningResult {
session_id,
problem_class,
diagnosis: ctx.diagnosis.clone(),
suggestions,
explanation: ctx.explanation.clone(),
actions_taken: outcomes,
overall_confidence,
total_latency_ms: total_latency,
};
self.store_session_fact(problem, &result);
Ok(result)
}
fn store_session_fact(&self, problem: &str, result: &ReasoningResult) {
if result.overall_confidence < 0.3 {
return; }
let fact_id = format!("session:{}", result.session_id);
let key = format!("reasoning:{}", result.problem_class);
let actions_str: Vec<String> = result
.actions_taken
.iter()
.map(|a| format!("{}({})", a.action, a.model_used))
.collect();
let value = format!(
"Problem: {}\nClass: {}\nDiagnosis: {}\nActions: {}\nConfidence: {:.0}%",
truncate(problem, 200),
result.problem_class,
truncate(&result.diagnosis, 300),
actions_str.join(" → "),
result.overall_confidence * 100.0,
);
let Ok(mut engine) = self.memgine.lock() else {
tracing::warn!("skipping session fact storage: lock poisoned");
return;
};
engine.ingest_fact(
&fact_id,
&key,
&value,
"car-reason",
"system",
Utc::now(),
"project",
None,
result.problem_class.keywords(),
false,
);
}
}
fn truncate(s: &str, max: usize) -> &str {
if s.len() <= max {
s
} else {
&s[..s.floor_char_boundary(max)]
}
}
fn extract_suggestions(ctx: &AccumulatedContext) -> Vec<CodeSuggestion> {
if ctx.code.is_empty() {
return vec![];
}
let mut suggestions = Vec::new();
let mut in_block = false;
let mut current_block = String::new();
for line in ctx.code.lines() {
if line.trim().starts_with("```") {
if in_block {
if !current_block.trim().is_empty() {
suggestions.push(CodeSuggestion {
file_path: None,
original: None,
suggested: current_block.trim().to_string(),
confidence: 0.7,
verification: VerificationStatus::NotVerified,
});
}
current_block.clear();
in_block = false;
} else {
in_block = true;
}
} else if in_block {
current_block.push_str(line);
current_block.push('\n');
}
}
if suggestions.is_empty() && !ctx.code.trim().is_empty() {
suggestions.push(CodeSuggestion {
file_path: None,
original: None,
suggested: ctx.code.trim().to_string(),
confidence: 0.5,
verification: VerificationStatus::NotVerified,
});
}
suggestions
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn extract_code_blocks() {
let mut ctx = AccumulatedContext::new("test", "", ProblemClass::BugFix);
ctx.code = "Here's the fix:\n```rust\nfn add(a: i32, b: i32) -> i32 { a + b }\n```\nThis fixes it.".into();
let suggestions = extract_suggestions(&ctx);
assert_eq!(suggestions.len(), 1);
assert!(suggestions[0].suggested.contains("fn add"));
}
#[test]
fn extract_no_code_blocks() {
let mut ctx = AccumulatedContext::new("test", "", ProblemClass::BugFix);
ctx.code = "Just change the + to a -".into();
let suggestions = extract_suggestions(&ctx);
assert_eq!(suggestions.len(), 1);
assert!(suggestions[0].suggested.contains("change the + to a -"));
}
}