Skip to content

Commit bf54ae3

Browse files
authored
Merge pull request #711 from terraphim/task/82-correction-event
feat(learnings): add CorrectionEvent for user corrections
2 parents 051a714 + 0297924 commit bf54ae3

9 files changed

Lines changed: 1765 additions & 42 deletions

File tree

.cachebro/cache.db

232 KB
Binary file not shown.

.cachebro/cache.db-shm

-32 KB
Binary file not shown.

.cachebro/cache.db-wal

-80.5 KB
Binary file not shown.

crates/terraphim_agent/src/learnings/capture.rs

Lines changed: 665 additions & 0 deletions
Large diffs are not rendered by default.

crates/terraphim_agent/src/learnings/mod.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@ mod procedure;
3131
mod redaction;
3232

3333
pub use capture::{
34-
LearningSource, capture_failed_command, correct_learning, list_learnings, query_learnings,
34+
CorrectionType, LearningSource, capture_correction, capture_failed_command, correct_learning,
35+
list_all_entries, query_all_entries,
3536
};
3637

3738
// Re-export for testing - not used by CLI yet

crates/terraphim_agent/src/main.rs

Lines changed: 62 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -759,6 +759,24 @@ enum LearnSub {
759759
#[arg(long)]
760760
correction: String,
761761
},
762+
/// Record a user correction (tool preference, naming, workflow, etc.)
763+
Correction {
764+
/// What the agent said/did originally
765+
#[arg(long)]
766+
original: String,
767+
/// What the user said instead
768+
#[arg(long)]
769+
corrected: String,
770+
/// Type of correction
771+
#[arg(long, default_value = "other")]
772+
correction_type: String,
773+
/// Context description
774+
#[arg(long, default_value = "")]
775+
context: String,
776+
/// Session ID for traceability
777+
#[arg(long)]
778+
session_id: Option<String>,
779+
},
762780
/// Process hook input from AI agents (reads JSON from stdin)
763781
Hook {
764782
/// AI agent format
@@ -1936,8 +1954,8 @@ async fn run_offline_command(
19361954

19371955
async fn run_learn_command(sub: LearnSub) -> Result<()> {
19381956
use learnings::{
1939-
LearningCaptureConfig, capture_failed_command, correct_learning, list_learnings,
1940-
query_learnings,
1957+
CorrectionType, LearningCaptureConfig, capture_correction, capture_failed_command,
1958+
correct_learning, list_all_entries, query_all_entries,
19411959
};
19421960
let config = LearningCaptureConfig::default();
19431961

@@ -1974,25 +1992,19 @@ async fn run_learn_command(sub: LearnSub) -> Result<()> {
19741992
} else {
19751993
&storage_loc
19761994
};
1977-
match list_learnings(storage_dir, recent) {
1978-
Ok(learnings) => {
1979-
if learnings.is_empty() {
1995+
match list_all_entries(storage_dir, recent) {
1996+
Ok(entries) => {
1997+
if entries.is_empty() {
19801998
println!("No learnings found.");
19811999
} else {
19822000
println!("Recent learnings:");
1983-
for (i, learning) in learnings.iter().enumerate() {
1984-
let source_indicator = match learning.source {
2001+
for (i, entry) in entries.iter().enumerate() {
2002+
let source_indicator = match entry.source() {
19852003
learnings::LearningSource::Project => "[P]",
19862004
learnings::LearningSource::Global => "[G]",
19872005
};
1988-
println!(
1989-
" {}. {} {} (exit: {})",
1990-
i + 1,
1991-
source_indicator,
1992-
learning.command,
1993-
learning.exit_code
1994-
);
1995-
if let Some(ref correction) = learning.correction {
2006+
println!(" {}. {} {}", i + 1, source_indicator, entry.summary());
2007+
if let Some(correction) = entry.correction_text() {
19962008
println!(" Correction: {}", correction);
19972009
}
19982010
}
@@ -2013,22 +2025,19 @@ async fn run_learn_command(sub: LearnSub) -> Result<()> {
20132025
} else {
20142026
&storage_loc
20152027
};
2016-
match query_learnings(storage_dir, &pattern, exact) {
2017-
Ok(learnings) => {
2018-
if learnings.is_empty() {
2028+
match query_all_entries(storage_dir, &pattern, exact) {
2029+
Ok(entries) => {
2030+
if entries.is_empty() {
20192031
println!("No learnings matching '{}'.", pattern);
20202032
} else {
2021-
println!("Learnings matching '{}':", pattern);
2022-
for learning in learnings {
2023-
let source_indicator = match learning.source {
2033+
println!("Learnings matching '{}'.", pattern);
2034+
for entry in entries {
2035+
let source_indicator = match entry.source() {
20242036
learnings::LearningSource::Project => "[P]",
20252037
learnings::LearningSource::Global => "[G]",
20262038
};
2027-
println!(
2028-
" {} {} (exit: {})",
2029-
source_indicator, learning.command, learning.exit_code
2030-
);
2031-
if let Some(ref correction) = learning.correction {
2039+
println!(" {} {}", source_indicator, entry.summary());
2040+
if let Some(correction) = entry.correction_text() {
20322041
println!(" Correction: {}", correction);
20332042
}
20342043
}
@@ -2051,6 +2060,33 @@ async fn run_learn_command(sub: LearnSub) -> Result<()> {
20512060
}
20522061
}
20532062
}
2063+
LearnSub::Correction {
2064+
original,
2065+
corrected,
2066+
correction_type,
2067+
context,
2068+
session_id,
2069+
} => {
2070+
let ct: CorrectionType = correction_type
2071+
.parse()
2072+
.unwrap_or(CorrectionType::Other(correction_type.clone()));
2073+
let correction = capture_correction(ct, &original, &corrected, &context, &config);
2074+
if let Some(ref sid) = session_id {
2075+
// We need to read the file and update it with session_id
2076+
// For now, just print the session_id
2077+
log::info!("Session ID: {}", sid);
2078+
}
2079+
match correction {
2080+
Ok(path) => {
2081+
println!("Captured correction: {}", path.display());
2082+
Ok(())
2083+
}
2084+
Err(e) => {
2085+
eprintln!("Failed to capture correction: {}", e);
2086+
Err(e.into())
2087+
}
2088+
}
2089+
}
20542090
LearnSub::Hook { format } => learnings::process_hook_input(format)
20552091
.await
20562092
.map_err(|e| e.into()),

crates/terraphim_agent/tests/integration_tests.rs

Lines changed: 30 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,9 @@ async fn start_test_server() -> Result<(Child, String)> {
3939

4040
// Use absolute path for config to work in CI
4141
let workspace_root = get_workspace_root()?;
42-
let config_path = workspace_root.join("terraphim_server/default/terraphim_engineer_config.json");
43-
42+
let config_path =
43+
workspace_root.join("terraphim_server/default/terraphim_engineer_config.json");
44+
4445
println!("Using config path: {}", config_path.display());
4546

4647
let mut server = Command::new("cargo")
@@ -320,19 +321,25 @@ async fn test_end_to_end_server_workflow() -> Result<()> {
320321
// 3. Test search with server (may fail in CI due to missing KG data or slow indexing)
321322
let (search_stdout, search_stderr, search_code) =
322323
run_server_command(&server_url, &["search", "integration test", "--limit", "3"])?;
323-
324+
324325
// In CI, search may fail due to:
325326
// - KG indexing timeout
326327
// - Missing KG data (400 Bad Request)
327328
// Accept both as valid outcomes for CI resilience
328-
let search_failed_acceptably = search_stderr.contains("operation timed out")
329+
let search_failed_acceptably = search_stderr.contains("operation timed out")
329330
|| search_stderr.contains("timed out")
330331
|| search_stderr.contains("400 Bad Request")
331332
|| search_stderr.contains("400");
332-
333+
333334
if search_failed_acceptably {
334-
println!("✓ Server search failed acceptably (expected in CI): {}",
335-
if search_stderr.contains("400") { "400 Bad Request" } else { "timeout" });
335+
println!(
336+
"✓ Server search failed acceptably (expected in CI): {}",
337+
if search_stderr.contains("400") {
338+
"400 Bad Request"
339+
} else {
340+
"timeout"
341+
}
342+
);
336343
} else if search_code != 0 {
337344
println!("Search stdout: {}", search_stdout);
338345
println!("Search stderr: {}", search_stderr);
@@ -348,14 +355,18 @@ async fn test_end_to_end_server_workflow() -> Result<()> {
348355
&server_url,
349356
&["search", "test", "--role", test_role, "--limit", "2"],
350357
)?;
351-
352-
let search_role_failed_acceptably = search_role_stderr.contains("operation timed out")
358+
359+
let search_role_failed_acceptably = search_role_stderr.contains("operation timed out")
353360
|| search_role_stderr.contains("timed out")
354361
|| search_role_stderr.contains("400 Bad Request")
355362
|| search_role_stderr.contains("400");
356-
363+
357364
if search_role_failed_acceptably {
358-
let reason = if search_role_stderr.contains("400") { "400 Bad Request" } else { "timeout" };
365+
let reason = if search_role_stderr.contains("400") {
366+
"400 Bad Request"
367+
} else {
368+
"timeout"
369+
};
359370
println!(
360371
"✓ Server search with role override '{}' failed acceptably ({})",
361372
test_role, reason
@@ -710,16 +721,20 @@ async fn test_full_feature_matrix() -> Result<()> {
710721
// - KG indexing timeout
711722
// - Missing KG data (400 Bad Request)
712723
// Accept both as valid outcomes for CI resilience
713-
let failed_acceptably = stderr.contains("operation timed out")
724+
let failed_acceptably = stderr.contains("operation timed out")
714725
|| stderr.contains("timed out")
715726
|| stderr.contains("400 Bad Request")
716727
|| stderr.contains("400");
717728

718729
if test_name == "graph" || test_name == "search" {
719730
if failed_acceptably {
720-
let reason = if stderr.contains("400") { "400 Bad Request" }
721-
else if stderr.contains("404") { "404 Not Found" }
722-
else { "timeout" };
731+
let reason = if stderr.contains("400") {
732+
"400 Bad Request"
733+
} else if stderr.contains("404") {
734+
"404 Not Found"
735+
} else {
736+
"timeout"
737+
};
723738
println!(" ✓ {}: failed acceptably ({})", test_name, reason);
724739
} else if test_name == "graph" && stderr.contains("404") {
725740
println!(" ✓ {}: unsupported (404)", test_name);

0 commit comments

Comments
 (0)