fix(ticket-assistant): rendre visible l'échec « aucune réponse » de l'assistant de ticket (#60)
Introduit un variant terminal `ReplyEvent::Error { message }` (domain/ports)
propagé jusqu'au chat : l'adapter OpenAI-compat récupère `reasoning_content`
quand `content` est vide, et un `Final` vide est converti en `Error` visible
plutôt qu'un tour silencieux qui pend. Le front (`ReplyChunk` + useTicketAssistant)
rend ce message d'erreur dans la cellule.
- domain: variant `ReplyEvent::Error`, bras non terminal (readiness, structured drain)
- infra/session: parse `reasoning_content` (delta/completion), fallback visible
- app-tauri: mapping ReplyEvent::Error -> ReplyChunk::Error, Final vide -> Error
- frontend: ReplyChunk `error`, rendu dans useTicketAssistant (+ test .test.tsx)
NB: commit sur la feature branch, PAS un merge. #60 reste inProgress.
Les 10 tests `cargo test -p infrastructure --lib session` échouent SOUS SANDBOX
uniquement (bind loopback 127.0.0.1:0 interdit -> Os PermissionDenied), pas une
régression : revérification hors-sandbox requise avant tout merge vers develop.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -47,8 +47,10 @@ pub struct ParsedLine {
|
||||
/// - `{"type":"turn.started"}` ⇒ [`ReplyEvent::Heartbeat`] (vivacité non terminale).
|
||||
/// - `{"type":"item.completed","item":{"id":"item_0","type":"agent_message","text":"…"}}`
|
||||
/// ⇒ si `item.type=="agent_message"` ⇒ [`ReplyEvent::Final`] (`content` = `item.text`,
|
||||
/// c'est la réponse) ; sinon (`reasoning`/`command`/autre) ⇒
|
||||
/// [`ReplyEvent::ToolActivity`] (`label` = `item.type`).
|
||||
/// c'est la réponse) ; si `item.type=="error"` ⇒ [`ReplyEvent::Error`] avec message
|
||||
/// extrait de `item.text`, `item.message`, `item.error.message`, `item.error`, puis
|
||||
/// des champs top-level `stderr`/`message`/`error.message`/`error` ; sinon
|
||||
/// (`reasoning`/`command`/autre) ⇒ [`ReplyEvent::ToolActivity`] (`label` = `item.type`).
|
||||
/// - `{"type":"turn.completed","usage":{…}}` ⇒ [`ReplyEvent::Heartbeat`] (le `Final`
|
||||
/// vient de l'`agent_message`, pas de `turn.completed`).
|
||||
///
|
||||
@ -91,6 +93,11 @@ pub fn parse_event(line: &str) -> Result<ParsedLine, AgentSessionError> {
|
||||
.to_owned();
|
||||
events.push(ReplyEvent::Final { content });
|
||||
}
|
||||
Some("error") => {
|
||||
events.push(ReplyEvent::Error {
|
||||
message: codex_error_message(&value, item),
|
||||
});
|
||||
}
|
||||
// reasoning / command / tout autre item ⇒ activité (label = type).
|
||||
Some(kind) => events.push(ReplyEvent::ToolActivity {
|
||||
label: kind.to_owned(),
|
||||
@ -109,6 +116,31 @@ pub fn parse_event(line: &str) -> Result<ParsedLine, AgentSessionError> {
|
||||
})
|
||||
}
|
||||
|
||||
const CODEX_ERROR_FALLBACK: &str = "Codex a renvoyé une erreur.";
|
||||
|
||||
fn non_blank_string(value: Option<&Value>) -> Option<String> {
|
||||
value
|
||||
.and_then(Value::as_str)
|
||||
.map(str::trim)
|
||||
.filter(|s| !s.is_empty())
|
||||
.map(str::to_owned)
|
||||
}
|
||||
|
||||
fn error_value_message(value: Option<&Value>) -> Option<String> {
|
||||
let value = value?;
|
||||
non_blank_string(value.get("message")).or_else(|| non_blank_string(Some(value)))
|
||||
}
|
||||
|
||||
fn codex_error_message(root: &Value, item: &Value) -> String {
|
||||
non_blank_string(item.get("text"))
|
||||
.or_else(|| non_blank_string(item.get("message")))
|
||||
.or_else(|| error_value_message(item.get("error")))
|
||||
.or_else(|| non_blank_string(root.get("stderr")))
|
||||
.or_else(|| non_blank_string(root.get("message")))
|
||||
.or_else(|| error_value_message(root.get("error")))
|
||||
.unwrap_or_else(|| CODEX_ERROR_FALLBACK.to_owned())
|
||||
}
|
||||
|
||||
/// Adapter de session structurée Codex.
|
||||
///
|
||||
/// Incarnation « un `codex exec` par tour » (§17.2 (b)) : chaque `send` relance
|
||||
|
||||
Reference in New Issue
Block a user