feat(inter-agent): backend des annonces live d'agent (B0-B3)
Redémarre le ticket #4 sur la base develop. Émission d'annonces live d'un agent vers l'UI, distinctes du Final de délégation : - domain: ReplyEvent::Announcement/Final et DomainEvent::AgentAnnouncement (events.rs), port d'émission (ports.rs), gating de readiness (readiness.rs). - application: mapping des événements structurés en annonces (agent/structured.rs, agent/mod.rs, lib.rs) et relais côté orchestrateur (orchestrator/service.rs). - infrastructure/session: parse des annonces + fix du Final pour Claude et Codex, propagé aux adaptateurs et à la conformance (claude.rs, codex.rs, conformance.rs, mod.rs, process.rs, sandbox_e2e.rs). - app-tauri: relais Tauri des annonces vers le front (events.rs, chat.rs). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -28,6 +28,7 @@
|
||||
|
||||
use std::io;
|
||||
use std::process::Stdio;
|
||||
use std::sync::mpsc::Sender;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
@ -82,13 +83,15 @@ pub async fn run_turn(
|
||||
spec: &SpawnLine,
|
||||
timeout: Option<Duration>,
|
||||
enforcer: Option<&Arc<dyn SandboxEnforcer>>,
|
||||
line_tap: Option<Sender<String>>,
|
||||
) -> Result<Vec<String>, AgentSessionError> {
|
||||
// Chemin SANDBOXÉ (Linux + plan posé + enforcer câblé) : transpose la technique
|
||||
// du PTY (`spawn_command_sandboxed`) — enforce sur un thread jetable AVANT le fork,
|
||||
// l'enfant hérite le domaine via fork+exec.
|
||||
#[cfg(target_os = "linux")]
|
||||
if let (Some(plan), Some(enforcer)) = (spec.sandbox.as_ref(), enforcer) {
|
||||
return run_turn_sandboxed(spec, plan.clone(), Arc::clone(enforcer), timeout).await;
|
||||
return run_turn_sandboxed(spec, plan.clone(), Arc::clone(enforcer), timeout, line_tap)
|
||||
.await;
|
||||
}
|
||||
// Hors Linux : aucun sandboxing OS ⇒ on ignore l'enforcer (Noop de toute façon) et
|
||||
// on garde le drain async historique. `let _` évite l'avertissement « unused ».
|
||||
@ -96,11 +99,11 @@ pub async fn run_turn(
|
||||
let _ = enforcer;
|
||||
|
||||
match timeout {
|
||||
Some(dur) => match tokio::time::timeout(dur, drain(spec)).await {
|
||||
Some(dur) => match tokio::time::timeout(dur, drain(spec, line_tap)).await {
|
||||
Ok(result) => result,
|
||||
Err(_elapsed) => Err(AgentSessionError::Timeout),
|
||||
},
|
||||
None => drain(spec).await,
|
||||
None => drain(spec, line_tap).await,
|
||||
}
|
||||
}
|
||||
|
||||
@ -141,6 +144,7 @@ async fn run_turn_sandboxed(
|
||||
plan: SandboxPlan,
|
||||
enforcer: Arc<dyn SandboxEnforcer>,
|
||||
timeout: Option<Duration>,
|
||||
line_tap: Option<Sender<String>>,
|
||||
) -> Result<Vec<String>, AgentSessionError> {
|
||||
use std::sync::Mutex as StdMutex;
|
||||
|
||||
@ -158,7 +162,9 @@ async fn run_turn_sandboxed(
|
||||
|
||||
// Thread JETABLE : sa restriction Landlock meurt avec lui.
|
||||
std::thread::spawn(move || {
|
||||
let result = drain_sandboxed(command, args, cwd, env, stdin, &enforcer, &plan, killer_tx);
|
||||
let result = drain_sandboxed(
|
||||
command, args, cwd, env, stdin, &enforcer, &plan, killer_tx, line_tap,
|
||||
);
|
||||
// Le récepteur peut avoir abandonné (timeout) : on ignore l'erreur d'envoi.
|
||||
let _ = done_tx.send(result);
|
||||
});
|
||||
@ -211,6 +217,7 @@ fn drain_sandboxed(
|
||||
enforcer: &Arc<dyn SandboxEnforcer>,
|
||||
plan: &SandboxPlan,
|
||||
killer_tx: tokio::sync::oneshot::Sender<Arc<std::sync::Mutex<std::process::Child>>>,
|
||||
line_tap: Option<Sender<String>>,
|
||||
) -> Result<Vec<String>, AgentSessionError> {
|
||||
use std::io::{BufRead, BufReader as StdBufReader, Write as _};
|
||||
use std::process::{Command as StdCommand, Stdio};
|
||||
@ -267,7 +274,12 @@ fn drain_sandboxed(
|
||||
let mut collected = Vec::new();
|
||||
for line in StdBufReader::new(stdout).lines() {
|
||||
match line {
|
||||
Ok(l) => collected.push(l),
|
||||
Ok(l) => {
|
||||
if let Some(tap) = &line_tap {
|
||||
let _ = tap.send(l.clone());
|
||||
}
|
||||
collected.push(l);
|
||||
}
|
||||
Err(e) => return Err(AgentSessionError::Io(e.to_string())),
|
||||
}
|
||||
}
|
||||
@ -280,7 +292,10 @@ fn drain_sandboxed(
|
||||
}
|
||||
|
||||
/// Cœur du drain : spawn → écriture stdin → lecture ligne-à-ligne → wait.
|
||||
async fn drain(spec: &SpawnLine) -> Result<Vec<String>, AgentSessionError> {
|
||||
async fn drain(
|
||||
spec: &SpawnLine,
|
||||
line_tap: Option<Sender<String>>,
|
||||
) -> Result<Vec<String>, AgentSessionError> {
|
||||
let mut cmd = Command::new(&spec.command);
|
||||
cmd.args(&spec.args)
|
||||
.stdin(Stdio::piped())
|
||||
@ -323,7 +338,12 @@ async fn drain(spec: &SpawnLine) -> Result<Vec<String>, AgentSessionError> {
|
||||
let mut collected = Vec::new();
|
||||
loop {
|
||||
match lines.next_line().await {
|
||||
Ok(Some(line)) => collected.push(line),
|
||||
Ok(Some(line)) => {
|
||||
if let Some(tap) = &line_tap {
|
||||
let _ = tap.send(line.clone());
|
||||
}
|
||||
collected.push(line);
|
||||
}
|
||||
Ok(None) => break,
|
||||
Err(e) => return Err(map_io(e)),
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user