Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions crates/spark-server/src/api/chat_phases.rs
Original file line number Diff line number Diff line change
Expand Up @@ -136,9 +136,10 @@ pub(super) fn apply_failure_guards(req: &mut ChatCompletionRequest) -> F23Progre
);
}

// F32: duplicate failed tool_result at conversation tail.
// F32: surface the most recent failed tool_result without creating
// an orphan role=tool message that vendor templates reject.
if f32_reposition_failed_tool_result(&mut req.messages) {
tracing::info!("F32: duplicated most-recent failed tool_result at conversation tail");
tracing::info!("F32: surfaced most-recent failed tool_result in a runtime reminder");
}

// F39: cross-turn permanent-failure circuit breaker.
Expand Down
10 changes: 8 additions & 2 deletions crates/spark-server/src/api/failures/circuit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,14 @@ pub fn f32_reposition_failed_tool_result(
{
return false;
}
let dup = messages[idx].clone();
messages.push(dup);
let failed_tool_result = messages[idx].content.text.trim();
let reminder = format!(
"\n\n<system-reminder>\nThe most recent failed tool result is still relevant. \
Treat it as an observed tool failure and do not retry the same call unless \
the approach materially changes.\n<failed_tool_result>\n{failed_tool_result}\n\
</failed_tool_result>\n</system-reminder>"
);
append_f7_reminder_to_last_user(messages, &reminder);
true
}

Expand Down
85 changes: 83 additions & 2 deletions crates/spark-server/src/api/failures/circuit_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,92 @@
//! `circuit.rs` stays under the 500-LoC file-size-cap.

use super::circuit::{
F39PermanentFailureMatch, f39_build_circuit_breaker_banner, f39_class_label,
f39_extract_binary_name,
F39PermanentFailureMatch, f32_reposition_failed_tool_result, f39_build_circuit_breaker_banner,
f39_class_label, f39_extract_binary_name,
};
use super::classification::F37FailureClass;

fn msg(role: &str, text: &str) -> crate::openai::IncomingMessage {
crate::openai::IncomingMessage {
role: role.to_string(),
content: crate::openai::ParsedContent {
text: text.to_string(),
images: Vec::new(),
},
tool_calls: None,
tool_call_id: None,
name: None,
}
}

fn assistant_tool_call(id: &str) -> crate::openai::IncomingMessage {
crate::openai::IncomingMessage {
role: "assistant".to_string(),
content: crate::openai::ParsedContent::default(),
tool_calls: Some(vec![crate::tool_parser::IncomingToolCall {
id: Some(id.to_string()),
function: crate::tool_parser::IncomingFunction {
name: "Bash".to_string(),
arguments: r#"{"command":"cargo"}"#.to_string(),
},
}]),
tool_call_id: None,
name: None,
}
}

fn tool_result(id: &str, text: &str) -> crate::openai::IncomingMessage {
crate::openai::IncomingMessage {
role: "tool".to_string(),
content: crate::openai::ParsedContent {
text: text.to_string(),
images: Vec::new(),
},
tool_calls: None,
tool_call_id: Some(id.to_string()),
name: None,
}
}

// ── f32_reposition_failed_tool_result ────────────────────────────

#[test]
fn f32_surfaces_error_without_orphan_tool_message() {
let mut messages = vec![
msg("user", "do it"),
assistant_tool_call("toolu_1"),
tool_result("toolu_1", "[tool error]\ncargo: command not found"),
msg("user", "intervening 1"),
msg("user", "intervening 2"),
];
let before = messages.len();

assert!(f32_reposition_failed_tool_result(&mut messages));

assert_eq!(messages.len(), before);
let last = messages.last().unwrap();
assert_eq!(last.role, "user");
assert!(last.content.text.contains("<failed_tool_result>"));
assert!(
last.content
.text
.contains("[tool error]\ncargo: command not found")
);
}

#[test]
fn f32_no_op_when_failed_tool_is_already_fresh() {
let mut messages = vec![
msg("user", "do it"),
assistant_tool_call("toolu_1"),
tool_result("toolu_1", "[tool error]\ncargo: command not found"),
];
let before = messages.len();

assert!(!f32_reposition_failed_tool_result(&mut messages));
assert_eq!(messages.len(), before);
}

// ── f39_extract_binary_name ───────────────────────────────────────

#[test]
Expand Down
Loading