Skip to content

Latest commit

 

History

History
476 lines (394 loc) · 12.9 KB

File metadata and controls

476 lines (394 loc) · 12.9 KB

AGENT_10: Live Task Progress

Mission

Implémenter l'affichage en temps réel du progrès des tâches, incluant les tool calls en cours, résultats intermédiaires, et updates TodoWrite live.

Contexte

Priorité: 🟠 Important
Effort estimé: 3-4 jours développeur
Présent dans: Droid

Objectifs Spécifiques

1. Progress Event System

// cortex-core/src/progress/events.rs

use tokio::sync::broadcast;

#[derive(Clone, Debug)]
pub enum ProgressEvent {
    /// Début d'une nouvelle tâche
    TaskStarted {
        task_id: String,
        description: String,
    },
    
    /// Appel d'outil en cours
    ToolCallStarted {
        task_id: String,
        tool_name: String,
        arguments: serde_json::Value,
    },
    
    /// Résultat d'outil
    ToolCallCompleted {
        task_id: String,
        tool_name: String,
        result: ToolResult,
        duration_ms: u64,
    },
    
    /// Mise à jour de la todo list
    TodoUpdated {
        task_id: String,
        todos: Vec<TodoItem>,
    },
    
    /// Token généré (pour streaming)
    TokenGenerated {
        task_id: String,
        token: String,
    },
    
    /// Pensée en cours
    ThinkingStarted {
        task_id: String,
    },
    
    /// Tâche complétée
    TaskCompleted {
        task_id: String,
        result: TaskResult,
        duration_ms: u64,
    },
    
    /// Erreur
    TaskError {
        task_id: String,
        error: String,
    },
}

#[derive(Clone, Debug)]
pub struct ToolResult {
    pub success: bool,
    pub output: Option<String>,
    pub error: Option<String>,
}

#[derive(Clone, Debug)]
pub struct TodoItem {
    pub text: String,
    pub status: TodoStatus,
}

#[derive(Clone, Copy, Debug, PartialEq)]
pub enum TodoStatus {
    Pending,
    InProgress,
    Completed,
}

pub struct ProgressEmitter {
    tx: broadcast::Sender<ProgressEvent>,
}

impl ProgressEmitter {
    pub fn new() -> (Self, ProgressSubscriber) {
        let (tx, rx) = broadcast::channel(1000);
        (Self { tx }, ProgressSubscriber { rx })
    }
    
    pub fn emit(&self, event: ProgressEvent) {
        let _ = self.tx.send(event);
    }
    
    pub fn subscribe(&self) -> ProgressSubscriber {
        ProgressSubscriber {
            rx: self.tx.subscribe(),
        }
    }
}

pub struct ProgressSubscriber {
    rx: broadcast::Receiver<ProgressEvent>,
}

impl ProgressSubscriber {
    pub async fn recv(&mut self) -> Option<ProgressEvent> {
        self.rx.recv().await.ok()
    }
}

2. TUI Progress Widget

// cortex-tui/src/widgets/progress.rs

use ratatui::prelude::*;

pub struct ProgressWidget {
    tasks: Vec<TaskProgress>,
    current_tool: Option<CurrentTool>,
    todos: Vec<TodoItem>,
}

#[derive(Clone)]
struct TaskProgress {
    id: String,
    description: String,
    status: TaskStatus,
    started_at: Instant,
}

#[derive(Clone)]
struct CurrentTool {
    name: String,
    arguments: String,
    started_at: Instant,
}

impl Widget for ProgressWidget {
    fn render(self, area: Rect, buf: &mut Buffer) {
        let chunks = Layout::default()
            .direction(Direction::Vertical)
            .constraints([
                Constraint::Length(3),  // Current task
                Constraint::Length(4),  // Current tool
                Constraint::Min(5),     // Todo list
            ])
            .split(area);
        
        // Current task
        self.render_current_task(chunks[0], buf);
        
        // Current tool call
        self.render_current_tool(chunks[1], buf);
        
        // Todo list
        self.render_todos(chunks[2], buf);
    }
}

impl ProgressWidget {
    fn render_current_task(&self, area: Rect, buf: &mut Buffer) {
        if let Some(task) = self.tasks.last() {
            let elapsed = task.started_at.elapsed();
            
            let block = Block::default()
                .title(Span::styled("Current Task", Style::default().fg(Color::Cyan)))
                .borders(Borders::ALL);
            
            let text = Paragraph::new(vec![
                Line::from(vec![
                    Span::styled("● ", Style::default().fg(Color::Yellow)),
                    Span::raw(&task.description),
                ]),
                Line::from(Span::styled(
                    format!("  {} elapsed", format_duration(elapsed)),
                    Style::default().fg(Color::DarkGray),
                )),
            ])
            .block(block);
            
            text.render(area, buf);
        }
    }
    
    fn render_current_tool(&self, area: Rect, buf: &mut Buffer) {
        let block = Block::default()
            .title(Span::styled("Tool Calls", Style::default().fg(Color::Green)))
            .borders(Borders::ALL);
        
        let content = if let Some(tool) = &self.current_tool {
            let elapsed = tool.started_at.elapsed();
            vec![
                Line::from(vec![
                    Span::styled("⚙ ", Style::default().fg(Color::Yellow)),
                    Span::styled(&tool.name, Style::default().add_modifier(Modifier::BOLD)),
                ]),
                Line::from(Span::styled(
                    format!("  {}", truncate(&tool.arguments, 50)),
                    Style::default().fg(Color::DarkGray),
                )),
                Line::from(Span::styled(
                    format!("  Running for {}...", format_duration(elapsed)),
                    Style::default().fg(Color::Yellow),
                )),
            ]
        } else {
            vec![Line::from(Span::styled("No active tool calls", Style::default().fg(Color::DarkGray)))]
        };
        
        Paragraph::new(content).block(block).render(area, buf);
    }
    
    fn render_todos(&self, area: Rect, buf: &mut Buffer) {
        let block = Block::default()
            .title(Span::styled("Todo List", Style::default().fg(Color::Magenta)))
            .borders(Borders::ALL);
        
        let items: Vec<ListItem> = self.todos.iter().map(|todo| {
            let (icon, style) = match todo.status {
                TodoStatus::Pending => ("○", Style::default().fg(Color::Gray)),
                TodoStatus::InProgress => ("◐", Style::default().fg(Color::Yellow)),
                TodoStatus::Completed => ("●", Style::default().fg(Color::Green)),
            };
            
            ListItem::new(Line::from(vec![
                Span::styled(format!("{} ", icon), style),
                Span::raw(&todo.text),
            ]))
        }).collect();
        
        List::new(items).block(block).render(area, buf);
    }
}

fn format_duration(d: Duration) -> String {
    let secs = d.as_secs();
    if secs < 60 {
        format!("{}s", secs)
    } else {
        format!("{}m {}s", secs / 60, secs % 60)
    }
}

3. Progress Collector

// cortex-tui/src/progress/collector.rs

pub struct ProgressCollector {
    subscriber: ProgressSubscriber,
    state: ProgressState,
}

#[derive(Default)]
struct ProgressState {
    active_tasks: HashMap<String, TaskProgress>,
    current_tool: Option<CurrentTool>,
    todos: Vec<TodoItem>,
    tool_history: VecDeque<ToolCallSummary>,
}

impl ProgressCollector {
    pub fn new(subscriber: ProgressSubscriber) -> Self {
        Self {
            subscriber,
            state: ProgressState::default(),
        }
    }
    
    /// Traite les événements et met à jour l'état
    pub async fn poll(&mut self) {
        while let Ok(event) = self.subscriber.rx.try_recv() {
            self.handle_event(event);
        }
    }
    
    fn handle_event(&mut self, event: ProgressEvent) {
        match event {
            ProgressEvent::TaskStarted { task_id, description } => {
                self.state.active_tasks.insert(task_id.clone(), TaskProgress {
                    id: task_id,
                    description,
                    status: TaskStatus::Running,
                    started_at: Instant::now(),
                });
            }
            
            ProgressEvent::ToolCallStarted { tool_name, arguments, .. } => {
                self.state.current_tool = Some(CurrentTool {
                    name: tool_name,
                    arguments: serde_json::to_string(&arguments).unwrap_or_default(),
                    started_at: Instant::now(),
                });
            }
            
            ProgressEvent::ToolCallCompleted { tool_name, result, duration_ms, .. } => {
                self.state.current_tool = None;
                self.state.tool_history.push_back(ToolCallSummary {
                    name: tool_name,
                    success: result.success,
                    duration_ms,
                });
                
                // Garder seulement les 10 derniers
                while self.state.tool_history.len() > 10 {
                    self.state.tool_history.pop_front();
                }
            }
            
            ProgressEvent::TodoUpdated { todos, .. } => {
                self.state.todos = todos;
            }
            
            ProgressEvent::TaskCompleted { task_id, .. } => {
                self.state.active_tasks.remove(&task_id);
            }
            
            ProgressEvent::TaskError { task_id, .. } => {
                if let Some(task) = self.state.active_tasks.get_mut(&task_id) {
                    task.status = TaskStatus::Failed;
                }
            }
            
            _ => {}
        }
    }
    
    /// Retourne le widget à afficher
    pub fn widget(&self) -> ProgressWidget {
        ProgressWidget {
            tasks: self.state.active_tasks.values().cloned().collect(),
            current_tool: self.state.current_tool.clone(),
            todos: self.state.todos.clone(),
        }
    }
}

4. Integration with Agent Loop

// cortex-engine/src/agent_loop.rs

impl AgentLoop {
    pub async fn run_with_progress(
        &mut self,
        prompt: &str,
        emitter: &ProgressEmitter,
    ) -> Result<AgentResult> {
        let task_id = uuid::Uuid::new_v4().to_string();
        
        emitter.emit(ProgressEvent::TaskStarted {
            task_id: task_id.clone(),
            description: truncate(prompt, 100).to_string(),
        });
        
        let result = self.run_inner(prompt, |event| {
            match event {
                AgentEvent::ToolCall { name, arguments } => {
                    emitter.emit(ProgressEvent::ToolCallStarted {
                        task_id: task_id.clone(),
                        tool_name: name,
                        arguments,
                    });
                }
                AgentEvent::ToolResult { name, result, duration } => {
                    emitter.emit(ProgressEvent::ToolCallCompleted {
                        task_id: task_id.clone(),
                        tool_name: name,
                        result,
                        duration_ms: duration.as_millis() as u64,
                    });
                }
                AgentEvent::TodoWrite { todos } => {
                    emitter.emit(ProgressEvent::TodoUpdated {
                        task_id: task_id.clone(),
                        todos,
                    });
                }
                _ => {}
            }
        }).await?;
        
        emitter.emit(ProgressEvent::TaskCompleted {
            task_id,
            result: result.clone(),
            duration_ms: self.elapsed().as_millis() as u64,
        });
        
        Ok(result)
    }
}

Tests

#[cfg(test)]
mod tests {
    #[tokio::test]
    async fn test_progress_events() {
        let (emitter, mut subscriber) = ProgressEmitter::new();
        
        emitter.emit(ProgressEvent::TaskStarted {
            task_id: "1".into(),
            description: "Test task".into(),
        });
        
        let event = subscriber.recv().await.unwrap();
        assert!(matches!(event, ProgressEvent::TaskStarted { .. }));
    }
    
    #[test]
    fn test_progress_widget_render() {
        let widget = ProgressWidget {
            tasks: vec![TaskProgress {
                id: "1".into(),
                description: "Test".into(),
                status: TaskStatus::Running,
                started_at: Instant::now(),
            }],
            current_tool: None,
            todos: vec![],
        };
        
        let mut buf = Buffer::empty(Rect::new(0, 0, 80, 20));
        widget.render(Rect::new(0, 0, 80, 20), &mut buf);
        
        // Check output contains expected text
        let content = buf.content.iter().map(|c| c.symbol()).collect::<String>();
        assert!(content.contains("Test"));
    }
}

Critères de Succès

  • Events émis pour chaque étape
  • Widget TUI affiche le progrès
  • Tool calls affichés en temps réel
  • Todo list mise à jour live
  • Tests passent

Estimation

Tâche Durée
Event system 0.5 jour
Progress widget 1 jour
Progress collector 0.5 jour
Agent loop integration 1 jour
Tests et documentation 0.5 jour
Total 3.5 jours

Agent autonome - Pas de dépendance externe