feat(backend): modèle unifié streaming/progress/events provider-agnostic + pont app-tauri — foundation #156 (QA verte)

This commit is contained in:
2026-08-06 13:13:17 +02:00
parent 40fa551f32
commit 918116664c
14 changed files with 630 additions and 59 deletions

View File

@ -15,7 +15,10 @@ use std::sync::{Arc, Mutex};
use async_trait::async_trait;
use serde_json::Value;
use domain::ports::{AgentSession, AgentSessionError, ReplyEvent, ReplyStream};
use domain::ports::{
AgentSession, AgentSessionError, ReplyEvent, ReplyProgress, ReplyProgressKind,
ReplyProgressSource, ReplyProgressStage, ReplyStream,
};
use domain::sandbox::{SandboxEnforcer, SandboxPlan};
use domain::SessionId;
@ -81,7 +84,28 @@ pub fn parse_event(line: &str) -> Result<ParsedLine, AgentSessionError> {
// Début/fin de tour côté moteur : pas de contenu, mais preuve de vivacité ⇒
// battement de cœur non terminal (readiness/heartbeat lot 1). Le `Final` vient
// toujours de l'`agent_message`, jamais de `turn.completed`.
Some("turn.started") | Some("turn.completed") => events.push(ReplyEvent::Heartbeat),
Some("turn.started") => {
events.push(ReplyEvent::Progress {
progress: provider_progress(
ReplyProgressKind::Turn,
ReplyProgressStage::Started,
"tour démarré",
"turn.started",
),
});
events.push(ReplyEvent::Heartbeat);
}
Some("turn.completed") => {
events.push(ReplyEvent::Progress {
progress: provider_progress(
ReplyProgressKind::Turn,
ReplyProgressStage::Completed,
"tour terminé côté provider",
"turn.completed",
),
});
events.push(ReplyEvent::Heartbeat);
}
Some("item.completed") => {
if let Some(item) = value.get("item") {
match item.get("type").and_then(Value::as_str) {
@ -99,9 +123,20 @@ pub fn parse_event(line: &str) -> Result<ParsedLine, AgentSessionError> {
});
}
// reasoning / command / tout autre item ⇒ activité (label = type).
Some(kind) => events.push(ReplyEvent::ToolActivity {
label: kind.to_owned(),
}),
Some(kind) => {
events.push(ReplyEvent::Progress {
progress: provider_progress(
ReplyProgressKind::Tool,
ReplyProgressStage::Completed,
kind,
"item.completed",
)
.with_tool_name(kind.to_owned()),
});
events.push(ReplyEvent::ToolActivity {
label: kind.to_owned(),
});
}
None => {}
}
}
@ -116,6 +151,16 @@ pub fn parse_event(line: &str) -> Result<ParsedLine, AgentSessionError> {
})
}
fn provider_progress(
kind: ReplyProgressKind,
stage: ReplyProgressStage,
label: impl Into<String>,
native_event: impl Into<String>,
) -> ReplyProgress {
ReplyProgress::new(ReplyProgressSource::ProviderNative, kind, stage, label)
.with_provider_event("codex", native_event)
}
const CODEX_ERROR_FALLBACK: &str = "Codex a renvoyé une erreur.";
fn non_blank_string(value: Option<&Value>) -> Option<String> {