feat(backend): modèle unifié streaming/progress/events provider-agnostic + pont app-tauri — foundation #156 (QA verte)
This commit is contained in:
@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user