magi-code 0.96.1

Repository-aware CLI coding agent for terminal work
Documentation
use super::*;

/// Parent-safe failure text; hook policy errors expose only their summary.
pub(super) fn subagent_failure_message(
    error: &anyhow::Error,
    retryable: bool,
    provider_retry_attempt: u64,
) -> String {
    let error = error.downcast_ref::<HookPolicyError>().map_or_else(
        || error.to_string(),
        |hook_error| hook_error.parent_safe_summary().to_string(),
    );
    if retryable && provider_retry_attempt >= SUBAGENT_PROVIDER_RETRY_MAX_ATTEMPTS {
        format!(
            "[provider retry exhausted after {} attempts] {error}",
            provider_retry_attempt.saturating_add(1)
        )
    } else {
        error
    }
}

pub(super) fn completed_subagent_result(
    id: String,
    task: SubagentTask,
    cwd: PathBuf,
    session: Option<&crate::sessions::Session>,
    output: AgentRunOutput,
    structured_output: Option<Value>,
    usage: Option<crate::output::NormalizedUsageAggregate>,
) -> SubagentTaskResult {
    let mut output_text = output.text;
    let output_truncated =
        truncate_string_field(&mut output_text, SUBAGENT_RESULT_OUTPUT_CHAR_LIMIT);
    SubagentTaskResult {
        id,
        status: SubagentStatus::Completed,
        intent: task.intent,
        agent: task.agent,
        identity: task.identity,
        cwd,
        session_id: session.map(|session| session.id().to_string()),
        session_path: session.map(|session| session.path().to_path_buf()),
        total_tokens: output.total_tokens,
        usage,
        changed_files: Vec::new(),
        output: output_text,
        structured_output,
        output_truncated,
        error: None,
    }
}

pub(in crate::subagents) struct FailedSubagentInput<'a> {
    pub(super) config: &'a SubagentRunConfig,
    pub(super) task_activity_id: ActivityId,
    pub(super) id: String,
    pub(super) task: SubagentTask,
    pub(super) cwd: PathBuf,
    pub(super) error: String,
    pub(super) finisher: &'a TaskActivityFinisher,
    pub(super) session: Option<&'a crate::sessions::Session>,
    pub(super) partial_output: Option<BoundedPartialOutput>,
    pub(super) usage: Option<crate::output::NormalizedUsageAggregate>,
}

pub(in crate::subagents) fn finish_failed_subagent(
    input: FailedSubagentInput<'_>,
) -> SubagentTaskResult {
    let FailedSubagentInput {
        config,
        task_activity_id,
        id,
        task,
        cwd,
        error,
        finisher,
        session,
        partial_output,
        usage,
    } = input;
    let partial_output = partial_output.filter(|output| !output.text.trim().is_empty());
    let output_truncated = partial_output
        .as_ref()
        .is_some_and(|output| output.truncated);
    let output = partial_output
        .map(|output| output.text)
        .or_else(|| failed_subagent_session_snapshot(session));
    let mut result = failed_result_with_session_and_output(
        id,
        task,
        cwd,
        session.map(|session| session.id().to_string()),
        session.map(|session| session.path().to_path_buf()),
        error,
        output,
    );
    result.usage = usage;
    result.output_truncated |= output_truncated;
    finisher.finish(config, task_activity_id, ActivityStatus::Failed);
    result
}

pub(in crate::subagents) fn failed_result_with_session(
    id: String,
    task: SubagentTask,
    cwd: PathBuf,
    session_id: Option<String>,
    session_path: Option<PathBuf>,
    error: String,
) -> SubagentTaskResult {
    failed_result_with_session_and_usage(id, task, cwd, session_id, session_path, error, None)
}

pub(in crate::subagents) fn failed_result_with_session_and_usage(
    id: String,
    task: SubagentTask,
    cwd: PathBuf,
    session_id: Option<String>,
    session_path: Option<PathBuf>,
    error: String,
    usage: Option<crate::output::NormalizedUsageAggregate>,
) -> SubagentTaskResult {
    failed_result_with_session_and_output_and_usage(
        id,
        task,
        cwd,
        session_id,
        session_path,
        error,
        None,
        usage,
    )
}

pub(in crate::subagents) fn failed_result_with_session_and_output(
    id: String,
    task: SubagentTask,
    cwd: PathBuf,
    session_id: Option<String>,
    session_path: Option<PathBuf>,
    error: String,
    output: Option<String>,
) -> SubagentTaskResult {
    failed_result_with_session_and_output_and_usage(
        id,
        task,
        cwd,
        session_id,
        session_path,
        error,
        output,
        None,
    )
}

#[expect(clippy::too_many_arguments)]
fn failed_result_with_session_and_output_and_usage(
    id: String,
    task: SubagentTask,
    cwd: PathBuf,
    session_id: Option<String>,
    session_path: Option<PathBuf>,
    error: String,
    output: Option<String>,
    usage: Option<crate::output::NormalizedUsageAggregate>,
) -> SubagentTaskResult {
    let mut output = output.unwrap_or_default();
    let output_was_truncated =
        truncate_string_field(&mut output, SUBAGENT_RESULT_OUTPUT_CHAR_LIMIT);
    SubagentTaskResult {
        id,
        status: SubagentStatus::Failed,
        intent: task.intent,
        agent: task.agent,
        identity: task.identity,
        cwd,
        session_id,
        session_path,
        total_tokens: None,
        usage,
        changed_files: Vec::new(),
        output,
        structured_output: None,
        output_truncated: output_was_truncated,
        error: Some(redact_sensitive_text(&error)),
    }
}

pub(in crate::subagents) fn format_duration(duration: Duration) -> String {
    format!("{:.3}s", duration.as_secs_f64())
}

pub(in crate::subagents) fn failed_subagent_session_snapshot(
    session: Option<&crate::sessions::Session>,
) -> Option<String> {
    let session = session?;
    let events = session
        .read_recent_events_tolerant(SNAPSHOT_EVENT_LIMIT, SNAPSHOT_BYTE_LIMIT)
        .ok()?
        .events;
    let mut assistant_chunks = String::new();
    let mut authoritative_assistant_output = None;
    let mut recovery = Vec::new();
    for event in events {
        match event.kind() {
            Some(SessionEventKind::AssistantChunk) => {
                if let Some(text) = event.payload.get("text").and_then(Value::as_str) {
                    assistant_chunks.push_str(text);
                }
            }
            Some(SessionEventKind::AssistantOutput) => {
                if let Some(text) = event.payload.get("text").and_then(Value::as_str) {
                    authoritative_assistant_output = Some(text.to_string());
                }
            }
            Some(SessionEventKind::ReasoningSummary) => {
                if let Some(text) = event.payload.get("text").and_then(Value::as_str) {
                    recovery.push(format!("reasoning summary: {text}"));
                }
            }
            Some(SessionEventKind::AbortRecovery) => {
                recovery.push(format!("abort recovery: {}", event.payload));
            }
            Some(SessionEventKind::TurnStatus) => {
                if let Some(payload) = event.turn_status_payload()
                    && matches!(
                        payload.status,
                        crate::sessions::TurnStatus::Cancelled
                            | crate::sessions::TurnStatus::Failed
                            | crate::sessions::TurnStatus::CompactionRequired
                    )
                    && let Some(text) = payload.assistant_text
                {
                    authoritative_assistant_output = Some(text);
                }
            }
            _ => {}
        }
    }
    let assistant = authoritative_assistant_output
        .filter(|text| !text.trim().is_empty())
        .unwrap_or(assistant_chunks);
    let mut snapshot = String::new();
    if !assistant.trim().is_empty() {
        snapshot.push_str("Partial subagent output before failure:\n");
        snapshot.push_str(assistant.trim());
        snapshot.push('\n');
    }
    for item in recovery {
        if !snapshot.is_empty() {
            snapshot.push('\n');
        }
        snapshot.push_str(&item);
    }
    if snapshot.trim().is_empty() {
        None
    } else {
        truncate_string_field(&mut snapshot, SUBAGENT_RESULT_OUTPUT_CHAR_LIMIT);
        Some(snapshot)
    }
}

pub(in crate::subagents) fn truncate_subagents_output(output: &mut SubagentsOutput) {
    for result in &mut output.results {
        let output_was_truncated =
            truncate_string_field(&mut result.output, SUBAGENT_RESULT_OUTPUT_CHAR_LIMIT);
        let error_was_truncated = result
            .error
            .as_mut()
            .is_some_and(|error| truncate_string_field(error, SUBAGENT_RESULT_ERROR_CHAR_LIMIT));
        result.output_truncated |= output_was_truncated || error_was_truncated;
    }
}

pub(in crate::subagents) fn truncate_string_field(value: &mut String, char_limit: usize) -> bool {
    let char_count = value.chars().count();
    if char_count <= char_limit {
        return false;
    }
    if char_limit == 0 {
        value.clear();
        return true;
    }
    let marker = format!("{SUBAGENT_TRUNCATION_MARKER}{char_count} limit={char_limit}]");
    let marker_len = marker.chars().count();
    if marker_len >= char_limit {
        *value = marker.chars().take(char_limit).collect();
        return true;
    }
    let keep = char_limit - marker_len;
    let mut truncated = value.chars().take(keep).collect::<String>();
    truncated.push_str(&marker);
    *value = truncated;
    true
}