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

@ -16,7 +16,10 @@ use tokio::io::{AsyncBufReadExt, AsyncReadExt, BufReader};
use tokio::process::Command;
use tokio::sync::Mutex as AsyncMutex;
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;
@ -187,12 +190,37 @@ fn parse_jsonl_turn_scoped(
for record in records {
match record.event {
ParsedEvent::StepStart => {
events.push(ReplyEvent::Progress {
progress: provider_progress(
ReplyProgressKind::Turn,
ReplyProgressStage::Started,
"étape démarrée",
"step_start",
),
});
events.push(ReplyEvent::Heartbeat);
}
ParsedEvent::StepFinish => {
events.push(ReplyEvent::Progress {
progress: provider_progress(
ReplyProgressKind::Turn,
ReplyProgressStage::Completed,
"étape terminée",
"step_finish",
),
});
events.push(ReplyEvent::Heartbeat);
}
ParsedEvent::ToolActivity(label) => {
events.push(ReplyEvent::Progress {
progress: provider_progress(
ReplyProgressKind::Tool,
ReplyProgressStage::Started,
label.clone(),
"tool_use",
)
.with_tool_name(label.clone()),
});
events.push(ReplyEvent::ToolActivity { label });
}
ParsedEvent::Text(text) => {
@ -222,6 +250,16 @@ fn parse_jsonl_turn_scoped(
Ok(events)
}
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("opencode", native_event)
}
fn current_turn_records(
lines: &[String],
expected_session_id: Option<&str>,
@ -539,6 +577,13 @@ mod tests {
use super::*;
fn without_progress(events: Vec<ReplyEvent>) -> Vec<ReplyEvent> {
events
.into_iter()
.filter(|event| !matches!(event, ReplyEvent::Progress { .. }))
.collect()
}
#[test]
fn split_command_prefix_handles_quotes_without_shell() {
assert_eq!(
@ -557,7 +602,7 @@ mod tests {
])
.unwrap();
assert_eq!(
events,
without_progress(events),
vec![
ReplyEvent::Heartbeat,
ReplyEvent::TextDelta {
@ -624,7 +669,7 @@ mod tests {
let lines: Vec<String> = opencode_script().into_iter().map(str::to_owned).collect();
let events = parse_jsonl_turn(&lines).unwrap();
assert_eq!(
events,
without_progress(events),
vec![
ReplyEvent::Heartbeat,
ReplyEvent::TextDelta {
@ -647,7 +692,7 @@ mod tests {
])
.unwrap();
assert_eq!(
events,
without_progress(events),
vec![
ReplyEvent::Heartbeat,
ReplyEvent::Error {
@ -779,7 +824,7 @@ exit 1
let events = session.send("prompt").await.unwrap().collect::<Vec<_>>();
assert_eq!(
events,
without_progress(events),
vec![
ReplyEvent::Heartbeat,
ReplyEvent::TextDelta {