fix(opencode): le Final textuel exploitable survit au changement de messageID du tour — sélection du message courant par priorité (texte non vide > erreur tardive > step/tool) élimine les NoReply sporadiques sur rendez-vous structurés — #254 (QA verte)
This commit is contained in:
@ -438,19 +438,7 @@ fn current_turn_records(
|
|||||||
records.push(record);
|
records.push(record);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let Some(current_message_id) = records.iter().rev().find_map(|record| {
|
let Some(current_message_id) = current_message_id_from_records(&records) else {
|
||||||
matches!(
|
|
||||||
record.event,
|
|
||||||
ParsedEvent::Text(_)
|
|
||||||
| ParsedEvent::ToolActivity { .. }
|
|
||||||
| ParsedEvent::StepStart
|
|
||||||
| ParsedEvent::StepFinish
|
|
||||||
| ParsedEvent::Error(_)
|
|
||||||
)
|
|
||||||
.then(|| record.message_id.as_deref())
|
|
||||||
.flatten()
|
|
||||||
.map(str::to_owned)
|
|
||||||
}) else {
|
|
||||||
return Ok(records);
|
return Ok(records);
|
||||||
};
|
};
|
||||||
Ok(records
|
Ok(records
|
||||||
@ -464,6 +452,38 @@ fn current_turn_records(
|
|||||||
.collect())
|
.collect())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn current_message_id_from_records(records: &[ParsedRecord]) -> Option<String> {
|
||||||
|
let latest_text = records
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.rev()
|
||||||
|
.find(|(_, record)| exploitable_text_record(record));
|
||||||
|
let latest_error = records
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.rev()
|
||||||
|
.find(|(_, record)| matches!(record.event, ParsedEvent::Error(_)));
|
||||||
|
match (latest_text, latest_error) {
|
||||||
|
(Some((text_index, _)), Some((error_index, error))) if error_index > text_index => {
|
||||||
|
error.message_id.clone()
|
||||||
|
}
|
||||||
|
(Some((_, text)), _) => text.message_id.clone(),
|
||||||
|
(None, Some((_, error))) => error.message_id.clone(),
|
||||||
|
(None, None) => records.iter().rev().find_map(|record| {
|
||||||
|
matches!(
|
||||||
|
record.event,
|
||||||
|
ParsedEvent::ToolActivity { .. } | ParsedEvent::StepStart | ParsedEvent::StepFinish
|
||||||
|
)
|
||||||
|
.then(|| record.message_id.clone())
|
||||||
|
.flatten()
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn exploitable_text_record(record: &ParsedRecord) -> bool {
|
||||||
|
matches!(&record.event, ParsedEvent::Text(text) if !text.trim().is_empty())
|
||||||
|
}
|
||||||
|
|
||||||
fn has_structured_terminal_event(events: &[ReplyEvent]) -> bool {
|
fn has_structured_terminal_event(events: &[ReplyEvent]) -> bool {
|
||||||
events
|
events
|
||||||
.iter()
|
.iter()
|
||||||
@ -991,6 +1011,50 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_jsonl_turn_keeps_assistant_text_when_terminal_event_uses_different_message_id() {
|
||||||
|
let events = parse_jsonl_turn(&[
|
||||||
|
r#"{"type":"step_start","sessionID":"s1","part":{"messageID":"a","type":"step-start"}}"#
|
||||||
|
.to_owned(),
|
||||||
|
r#"{"type":"text","sessionID":"s1","part":{"messageID":"a","type":"text","text":"réponse utile"}}"#
|
||||||
|
.to_owned(),
|
||||||
|
r#"{"type":"step_finish","sessionID":"s1","part":{"messageID":"b","type":"step-finish"}}"#
|
||||||
|
.to_owned(),
|
||||||
|
])
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
events.last(),
|
||||||
|
Some(&ReplyEvent::Final {
|
||||||
|
content: "réponse utile".to_owned()
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_jsonl_turn_keeps_later_error_instead_of_promoting_earlier_text_to_final() {
|
||||||
|
let events = parse_jsonl_turn(&[
|
||||||
|
r#"{"type":"text","sessionID":"s1","part":{"messageID":"a","type":"text","text":"réponse partielle"}}"#
|
||||||
|
.to_owned(),
|
||||||
|
r#"{"type":"error","sessionID":"s1","part":{"messageID":"b"},"error":"provider failed"}"#
|
||||||
|
.to_owned(),
|
||||||
|
])
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
events
|
||||||
|
.iter()
|
||||||
|
.any(|event| matches!(event, ReplyEvent::Error { message } if message == "provider failed")),
|
||||||
|
"a later provider error must stay visible: {events:?}"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!events
|
||||||
|
.iter()
|
||||||
|
.any(|event| matches!(event, ReplyEvent::Final { .. })),
|
||||||
|
"a provider error must not be masked as a successful final: {events:?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parse_jsonl_turn_scoped_ignores_other_sessions_when_known() {
|
fn parse_jsonl_turn_scoped_ignores_other_sessions_when_known() {
|
||||||
let events = parse_jsonl_turn_scoped(
|
let events = parse_jsonl_turn_scoped(
|
||||||
|
|||||||
Reference in New Issue
Block a user