feat(engine): wire reflection pipeline + trace analysis into thread lifecycle

After every thread completes, ThreadManager now automatically runs:

1. Retrospective trace analysis (non-LLM, always):
   - Detects 8 issue categories (tool errors, code errors, missing
     outputs, excessive steps, hallucination risk, etc.)
   - Logs issues at warn level when found

2. Trace file recording (when ENGINE_V2_TRACE=1):
   - Writes full JSON trace to engine_trace_{timestamp}.json

3. LLM reflection (when enable_reflection=true):
   - Calls reflection pipeline to produce Summary, Lesson, Issue docs
   - Saves docs to store for future context retrieval
   - Enabled by default in the bridge router

All three run inside the spawned tokio task after exec.run() completes,
before saving the final thread state. No external wiring needed.

Removed duplicate trace recording from the router — it's now handled
by ThreadManager automatically.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
This commit is contained in:
2026-03-22 22:56:21 -07:00
co-authored by Claude Opus 4.6
parent b57178c1d1
commit b1254cf6f9
2 changed files with 48 additions and 10 deletions
+41 -1
View File
@@ -156,11 +156,51 @@ impl ThreadManager {
// Spawn background task
let store_for_task = Arc::clone(&self.store);
let llm_for_reflection = Arc::clone(&self.llm);
let handle = tokio::spawn(async move {
let mut exec = exec_loop;
let result = exec.run().await;
debug!(thread_id = %thread_id, "thread execution finished");
// Save final thread state to store (for trace recording)
// Run retrospective trace analysis (non-LLM, always runs)
let trace = crate::executor::trace::build_trace(&exec.thread);
if !trace.issues.is_empty() {
crate::executor::trace::log_trace_summary(&trace);
}
// Write trace file if enabled
if crate::executor::trace::is_trace_enabled() {
crate::executor::trace::write_trace(&trace);
}
// Run LLM reflection if enabled and thread completed
if exec.thread.config.enable_reflection
&& (exec.thread.state == crate::types::thread::ThreadState::Completed
|| exec.thread.state == crate::types::thread::ThreadState::Done)
{
debug!(thread_id = %thread_id, "running reflection pipeline");
match crate::reflection::reflect(&exec.thread, &llm_for_reflection).await {
Ok(reflection) => {
debug!(
thread_id = %thread_id,
docs = reflection.docs.len(),
tokens = reflection.tokens_used.total(),
"reflection complete"
);
for doc in &reflection.docs {
let _ = store_for_task.save_memory_doc(doc).await;
}
}
Err(e) => {
tracing::warn!(
thread_id = %thread_id,
"reflection failed: {e}"
);
}
}
}
// Save final thread state to store
let _ = store_for_task.save_thread(&exec.thread).await;
result
});
+7 -9
View File
@@ -168,7 +168,10 @@ pub async fn handle_with_engine(
content,
state.default_project_id,
&message.user_id,
ThreadConfig::default(),
ThreadConfig {
enable_reflection: true,
..ThreadConfig::default()
},
)
.await
.map_err(|e| {
@@ -222,14 +225,9 @@ pub async fn handle_with_engine(
.record_thread_outcome(conv_id, thread_id, &outcome)
.await;
// Trace recording + retrospective analysis
if ironclaw_engine::executor::trace::is_trace_enabled()
&& let Ok(Some(thread)) = state.store.load_thread(thread_id).await
{
let trace = ironclaw_engine::executor::trace::build_trace(&thread);
ironclaw_engine::executor::trace::log_trace_summary(&trace);
ironclaw_engine::executor::trace::write_trace(&trace);
}
// Note: trace recording, retrospective analysis, and LLM reflection
// all run automatically inside ThreadManager after the thread completes.
// See crates/ironclaw_engine/src/runtime/manager.rs.
// Convert outcome to response
match outcome {