magi-code 0.64.0

Repository-aware CLI coding agent for terminal work
Documentation
use super::{config::SubagentRunConfig, dto::SubagentTask, worker::subagent_prompt};
use crate::{
    agent::AgentOutputSink,
    agent::cancellation::AgentCancellation,
    output::{
        ActivityEvent, ActivityId, ActivityKind, ActivityMetadata, ActivitySender, ActivityStatus,
        OutputEvent, ToolStatus,
    },
};
use std::sync::Arc;

pub(super) fn subagent_task_activity_metadata(
    id: &str,
    task: &SubagentTask,
    depth: usize,
) -> ActivityMetadata {
    let mut metadata = ActivityMetadata::new(format!("{} · depth {} · {}", id, depth, task.intent));
    metadata.detail = Some(subagent_prompt(id, task, None));
    metadata
        .fields
        .push(("depth".to_string(), depth.to_string()));
    if let Some(identity) = &task.identity {
        metadata
            .fields
            .push(("identity".to_string(), identity.clone()));
    }
    if let Some(agent) = &task.agent {
        metadata.fields.push(("agent".to_string(), agent.clone()));
    }
    metadata
}

pub(super) fn emit_activity(config: &SubagentRunConfig, event: ActivityEvent) {
    if let Some(sender) = &config.activity_sender {
        sender(event);
    }
}

pub(super) struct SubagentActivitySink {
    pub(super) parent_id: ActivityId,
    pub(super) activity_sender: Option<ActivitySender>,
    pub(super) assistant_id: ActivityId,
    pub(super) assistant_started: bool,
    pub(super) reasoning_summary_group_count: usize,
    pub(super) reasoning_summary_group_lines: Vec<String>,
    pub(super) progress_reporter: Option<Arc<dyn Fn() + Send + Sync>>,
    pub(super) cancellation: AgentCancellation,
}

impl SubagentActivitySink {
    pub(super) fn finish_assistant(&mut self, status: ActivityStatus) {
        if !self.assistant_started {
            return;
        }
        self.assistant_started = false;
        if self.cancellation.is_canceled()
            && matches!(status, ActivityStatus::Canceled | ActivityStatus::Failed)
        {
            if let Some(sender) = &self.activity_sender {
                sender(ActivityEvent::Finished {
                    id: self.assistant_id.clone(),
                    status: ActivityStatus::Canceled,
                    metadata: None,
                });
            }
            return;
        }
        let _ = self.emit_activity_event_raw(ActivityEvent::Finished {
            id: self.assistant_id.clone(),
            status,
            metadata: None,
        });
    }

    fn emit_reasoning_summary_lines(&mut self, text: &str) -> anyhow::Result<()> {
        let lines = text
            .lines()
            .map(str::trim)
            .filter(|line| !line.is_empty())
            .map(str::to_string)
            .collect::<Vec<_>>();
        if lines.is_empty() {
            return Ok(());
        }

        if self.reasoning_summary_group_lines.is_empty() {
            self.reasoning_summary_group_count += 1;
        }
        self.reasoning_summary_group_lines.extend(lines);

        let line_count = self.reasoning_summary_group_lines.len();
        let mut metadata = ActivityMetadata::new(format!("reasoning summaries ×{line_count}"));
        metadata.detail = Some(numbered_reasoning_lines(
            &self.reasoning_summary_group_lines,
        ));
        self.emit_activity_event_raw(ActivityEvent::Started {
            id: ActivityId::new(format!(
                "{}/reasoning/{}",
                self.parent_id.as_str(),
                self.reasoning_summary_group_count
            )),
            parent_id: Some(self.parent_id.clone()),
            kind: ActivityKind::Assistant,
            status: ActivityStatus::Success,
            metadata,
        })?;
        Ok(())
    }

    fn close_reasoning_summary_group(&mut self) {
        self.reasoning_summary_group_lines.clear();
    }

    pub(super) fn emit_activity_event_raw(&mut self, event: ActivityEvent) -> anyhow::Result<()> {
        let allowed_after_cancel = matches!(
            event,
            ActivityEvent::Started {
                kind: ActivityKind::Tool,
                ..
            } | ActivityEvent::ToolStartedDetail { .. }
                | ActivityEvent::ToolResultDetail { .. }
                | ActivityEvent::Finished { .. }
        );
        if self.cancellation.is_canceled() && !allowed_after_cancel {
            return Ok(());
        }
        if let Some(progress) = &self.progress_reporter {
            progress();
        }
        if let Some(sender) = &self.activity_sender {
            sender(event);
        }
        Ok(())
    }
}

impl AgentOutputSink for SubagentActivitySink {
    fn assistant_delta(&mut self, text: &str) -> anyhow::Result<()> {
        if self.cancellation.is_canceled() {
            return Ok(());
        }
        self.close_reasoning_summary_group();
        if !self.assistant_started {
            self.assistant_started = true;
            self.activity_event(ActivityEvent::Started {
                id: self.assistant_id.clone(),
                parent_id: Some(self.parent_id.clone()),
                kind: ActivityKind::Assistant,
                status: ActivityStatus::Running,
                metadata: ActivityMetadata::new("assistant"),
            })?;
        }
        self.activity_event(ActivityEvent::Delta {
            id: self.assistant_id.clone(),
            preview: text.to_string(),
        })
    }

    fn output_event(&mut self, event: OutputEvent) -> anyhow::Result<()> {
        if self.cancellation.is_canceled()
            && !matches!(
                event,
                OutputEvent::ToolStarted { .. } | OutputEvent::ToolResult { .. }
            )
        {
            return Ok(());
        }
        match event {
            OutputEvent::ToolStarted { call, label } => {
                self.close_reasoning_summary_group();
                let status = crate::output::pending_activity_status(&call);
                self.activity_event(ActivityEvent::ToolStartedDetail {
                    id: ActivityId::new(call.id.clone()),
                    detail: crate::tool_display::tool_activity_detail_pending(&call, label, status),
                })
            }
            OutputEvent::ToolResult {
                call,
                result,
                summary,
            } => {
                let status = match summary.status {
                    ToolStatus::Running => ActivityStatus::Running,
                    ToolStatus::Writing => ActivityStatus::Writing,
                    ToolStatus::Success => ActivityStatus::Success,
                    ToolStatus::Failure => ActivityStatus::Failed,
                };
                self.activity_event(ActivityEvent::ToolResultDetail {
                    id: ActivityId::new(call.id.clone()),
                    detail: crate::tool_display::tool_activity_detail(
                        &call,
                        &result,
                        summary.label.clone(),
                        status,
                    ),
                })
            }
            OutputEvent::AssistantDelta { text } => self.assistant_delta(&text),
            OutputEvent::ThinkingSummaryComplete { text } => {
                self.emit_reasoning_summary_lines(&text)
            }
            OutputEvent::ThinkingSummaryCompleteIdentified { text, .. } => {
                self.emit_reasoning_summary_lines(&text)
            }
            OutputEvent::ContextUsage {
                current_tokens,
                max_tokens,
                reasoning_tokens,
                source,
                request_sequence,
            } => self.activity_event(ActivityEvent::UsageUpdate {
                id: self.parent_id.clone(),
                current_tokens,
                max_tokens,
                reasoning_tokens,
                source,
                request_sequence,
            }),
            OutputEvent::ThinkingSummaryDelta { .. } => Ok(()),
            OutputEvent::SessionHeader { .. }
            | OutputEvent::UserPrompt { .. }
            | OutputEvent::AutomaticUserPrompt { .. }
            | OutputEvent::CompactionTriggered { .. }
            | OutputEvent::CompactionStarted
            | OutputEvent::CompactionCompleted { .. }
            | OutputEvent::BashCommand { .. }
            | OutputEvent::Diagnostic { .. }
            | OutputEvent::HookDiagnostic { .. }
            | OutputEvent::ProviderContextInjection { .. }
            | OutputEvent::SubdirInstructionInjection { .. }
            | OutputEvent::AssistantComplete { .. } => {
                self.close_reasoning_summary_group();
                Ok(())
            }
        }
    }

    fn activity_event(&mut self, event: ActivityEvent) -> anyhow::Result<()> {
        if !matches!(event, ActivityEvent::UsageUpdate { .. }) {
            self.close_reasoning_summary_group();
        }
        self.emit_activity_event_raw(event)
    }

    fn activity_sender(&self) -> Option<ActivitySender> {
        self.activity_sender.clone()
    }

    fn current_parent_activity_id(&self) -> Option<ActivityId> {
        Some(self.parent_id.clone())
    }

    fn tool_block(&mut self, _block: &str) -> anyhow::Result<()> {
        Ok(())
    }
}

pub(super) fn numbered_reasoning_lines(lines: &[String]) -> String {
    lines
        .iter()
        .enumerate()
        .map(|(index, line)| format!("{}. {line}", index + 1))
        .collect::<Vec<_>>()
        .join("\n")
}