magi-code 0.100.0

Repository-aware CLI coding agent for terminal work
Documentation
use super::{
    ToolResult, ToolResultDisplay,
    args::{FILE_READ_MAX_BYTES, FILE_READ_MAX_FILES, FILE_WRITE_MAX_BYTES, ReadArgs, WriteArgs},
    contract::{metadata_key as meta, tool_name},
    skill_provenance::digest_hex,
};
use crate::{
    cancellation::AgentCancellation,
    checkpoints::SnapshotTool,
    persistence::{CrossProcessFileLock, atomic_write},
};
use serde_json::{Value, json};
use std::{
    fs,
    path::{Path, PathBuf},
    sync::Mutex,
};

mod mutation;
mod path_policy;
mod path_suggestions;
mod read_format;
pub(super) use mutation::MutationContext;
pub(super) use path_policy::{ExistingPathPolicy, PathContext, resolve_existing_path_with_policy};
use read_format::{MultiReadAppend, append_multi_read_section};

pub(super) struct ReadSnapshot {
    pub(super) path: PathBuf,
    pub(super) text: String,
    pub(super) seen_lines: Vec<usize>,
}

pub(super) struct ReadContext<'a> {
    pub(super) paths: PathContext<'a>,
    pub(super) settings: &'a crate::config::ReadToolSettings,
    pub(super) hashline_snapshots: &'a Mutex<super::hash_edit::HashlineSnapshotStore>,
    pub(super) pending_snapshots: Option<&'a Mutex<Vec<ReadSnapshot>>>,
    pub(super) skills: super::skill::SkillContext<'a>,
}

pub(super) struct WriteContext<'a> {
    pub(super) paths: PathContext<'a>,
    pub(super) settings: &'a crate::config::WriteToolSettings,
    pub(super) mutation: MutationContext<'a>,
    pub(super) humanize_protection: &'a Option<crate::protection::humanize::HumanizeProtection>,
}

impl ReadContext<'_> {
    pub(super) fn read(
        &self,
        args: ReadArgs,
        cancellation: &AgentCancellation,
    ) -> anyhow::Result<ToolResult> {
        if !args.is_multi_file() {
            let requested_path = args
                .requested_paths()
                .first()
                .ok_or_else(|| anyhow::anyhow!("read requires paths"))?;
            let mut resource = self.read_one_resource(requested_path, cancellation)?;
            let delivered_bytes = resource.content.len();
            self.register_read_snapshot(&mut resource, delivered_bytes)?;
            let mut metadata = resource.metadata;
            if let Some(provenance) = resource.provenance {
                metadata.insert("skill_reads".to_string(), json!([provenance]));
            }
            metadata.insert(meta::BYTE_LIMIT.to_string(), json!(FILE_READ_MAX_BYTES));
            metadata.insert(
                "structured_result".to_string(),
                json!({
                    "files": [{
                        "path": requested_path, "text": resource.content,
                        "success": true,
                        "complete": !resource.truncated,
                        "request_complete": resource.request_complete,
                    }],
                    "complete": !resource.truncated,
                    "request_complete": resource.request_complete,
                    "outcome": if resource.request_complete { "complete" } else { "partial" },
                }),
            );
            return Ok(ToolResult {
                tool_name: tool_name::READ.to_string(),
                success: true,
                content: resource.content,
                metadata: Value::Object(metadata),
                display: ToolResultDisplay::default(),
            });
        }

        let requested_paths = args.requested_paths();
        let mut output = String::new();
        let mut summaries = Vec::with_capacity(requested_paths.len());
        let mut succeeded = 0usize;
        let mut failed = 0usize;
        let mut omitted = 0usize;
        let mut truncated = false;
        let mut aggregate_truncated = false;
        let mut skill_reads = Vec::new();
        let mut files = Vec::new();

        for (index, requested_path) in requested_paths.iter().enumerate() {
            match self.read_one_resource(requested_path, cancellation) {
                Ok(mut resource) => {
                    let appended = append_multi_read_section(
                        &mut output,
                        requested_path,
                        &resource.content,
                        index + 1 < requested_paths.len(),
                    );
                    match appended {
                        MultiReadAppend::Complete => {
                            let delivered_bytes = resource.content.len();
                            self.register_read_snapshot(&mut resource, delivered_bytes)?;
                        }
                        MultiReadAppend::Truncated { body_bytes } => {
                            self.register_read_snapshot(&mut resource, body_bytes)?;
                        }
                        MultiReadAppend::Omitted => {}
                    }
                    let body = resource.content;
                    let provenance = resource.provenance;
                    let mut summary = resource.metadata;
                    summary.insert("requested_path".to_string(), json!(requested_path));
                    match appended {
                        MultiReadAppend::Complete => {
                            files.push(json!({"path": requested_path, "text": body,
                                "success": true, "complete": !resource.truncated,
                                "request_complete": resource.request_complete}));
                            succeeded += 1;
                            truncated |= resource.truncated;
                            summary.insert("success".to_string(), json!(true));
                            summaries.push(Value::Object(summary));
                            if let Some(mut provenance) = provenance {
                                provenance.start = output.len() - body.len();
                                provenance.end = output.len();
                                provenance.sha256 = digest_hex(
                                    &output.as_bytes()[provenance.start..provenance.end],
                                );
                                skill_reads.push(provenance);
                            }
                        }
                        MultiReadAppend::Truncated { body_bytes } => {
                            files.push(json!({"path": requested_path, "text": &body[..body_bytes],
                                "success": true, "complete": false, "request_complete": false,
                                "error": "aggregate read output limit exceeded"}));
                            succeeded += 1;
                            omitted = requested_paths.len() - index - 1;
                            aggregate_truncated = true;
                            truncated = true;
                            summary.insert("success".to_string(), json!(true));
                            summary.insert("truncated".to_string(), json!(true));
                            summary.insert("request_complete".to_string(), json!(false));
                            summaries.push(Value::Object(summary));
                            summaries.extend(requested_paths[index + 1..].iter().map(|path| {
                                json!({
                                    "requested_path": path,
                                    "success": false,
                                    "omitted": true,
                                    "error": "aggregate read output limit exceeded",
                                })
                            }));
                            break;
                        }
                        MultiReadAppend::Omitted => {
                            files.push(
                                json!({"path": requested_path, "text": "", "success": false,
                                "omitted": true, "complete": false, "request_complete": false,
                                "error": "aggregate read output limit exceeded"}),
                            );
                            aggregate_truncated = true;
                            truncated = true;
                            omitted = requested_paths.len() - index;
                            summary.insert("success".to_string(), json!(false));
                            summary.insert("omitted".to_string(), json!(true));
                            summary.insert("request_complete".to_string(), json!(false));
                            summary.insert(
                                "error".to_string(),
                                json!("aggregate read output limit exceeded"),
                            );
                            summaries.push(Value::Object(summary));
                            summaries.extend(requested_paths[index + 1..].iter().map(|path| {
                                json!({
                                    "requested_path": path,
                                    "success": false,
                                    "omitted": true,
                                    "error": "aggregate read output limit exceeded",
                                })
                            }));
                            break;
                        }
                    }
                }
                Err(error) => {
                    failed += 1;
                    files.push(json!({"path": requested_path, "text": "", "success": false,
                        "complete": false, "request_complete": false, "error": error.to_string()}));
                    summaries.push(json!({
                        "requested_path": requested_path,
                        "success": false,
                        "error": error.to_string(),
                    }));
                    let error_text = format!("ERROR: {error}");
                    if append_multi_read_section(
                        &mut output,
                        requested_path,
                        &error_text,
                        index + 1 < requested_paths.len(),
                    ) != MultiReadAppend::Complete
                    {
                        aggregate_truncated = true;
                        truncated = true;
                        omitted = requested_paths.len().saturating_sub(index + 1);
                        summaries.extend(requested_paths[index + 1..].iter().map(|path| {
                            json!({
                                "requested_path": path,
                                "success": false,
                                "omitted": true,
                                "error": "aggregate read output limit exceeded",
                            })
                        }));
                        break;
                    }
                }
            }
        }

        files.extend(requested_paths[files.len()..].iter().map(|path| {
            json!({"path": path, "text": "", "success": false, "omitted": true,
                "complete": false, "request_complete": false,
                "error": "aggregate read output limit exceeded"})
        }));

        let request_complete = files.iter().all(|file| file["request_complete"] == true);
        let outcome = if succeeded == 0 {
            "failed"
        } else if request_complete {
            "complete"
        } else {
            "partial"
        };

        if failed > 0 || truncated || aggregate_truncated || skill_reads.len() > 8 {
            skill_reads.clear();
        }
        Ok(ToolResult {
            tool_name: tool_name::READ.to_string(),
            success: succeeded > 0,
            content: output,
            metadata: json!({
                (meta::PATHS): requested_paths,
                (meta::FILES): requested_paths.len(),
                (meta::SUCCEEDED): succeeded,
                (meta::FAILED): failed,
                "omitted": omitted,
                (meta::BYTE_LIMIT): FILE_READ_MAX_BYTES,
                "file_limit": FILE_READ_MAX_FILES,
                (meta::TRUNCATED): truncated,
                "aggregate_truncated": aggregate_truncated,
                "results": summaries,
                "skill_reads": skill_reads,
                "structured_result": {
                    "files": files,
                    "complete": failed == 0 && !truncated && !aggregate_truncated,
                    "request_complete": request_complete,
                    "outcome": outcome,
                },
            }),
            display: ToolResultDisplay::default(),
        })
    }
}

impl WriteContext<'_> {
    pub(super) fn write_file_outcome(
        &self,
        args: WriteArgs,
        cancellation: &AgentCancellation,
    ) -> anyhow::Result<super::dispatch::FilesystemOutcome> {
        self.write_file_with(args, cancellation, atomic_write)
    }

    fn write_file_with(
        &self,
        args: WriteArgs,
        cancellation: &AgentCancellation,
        write: impl FnOnce(&Path, &[u8]) -> anyhow::Result<()>,
    ) -> anyhow::Result<super::dispatch::FilesystemOutcome> {
        cancellation.check()?;
        if args.content.len() > FILE_WRITE_MAX_BYTES {
            anyhow::bail!(
                "write content is {} bytes; limit is {FILE_WRITE_MAX_BYTES} bytes",
                args.content.len()
            );
        }
        let path = self.resolve_for_write(&args.path)?;
        let lock = self.mutation.file_lock(&path)?;
        let _guard = lock
            .lock()
            .map_err(|_| anyhow::anyhow!("file mutation lock poisoned for '{}'", path.display()))?;
        let _cross_process_guard = CrossProcessFileLock::acquire(&path)?;
        let pre_snapshot = self.mutation.pre_snapshot_bytes(&path)?;
        let prose_check = if let Some(protection) = &self.humanize_protection {
            let before = (|| -> anyhow::Result<String> {
                match crate::persistence::open_regular_file(&path, false) {
                    Ok(file) => Ok(String::from_utf8(
                        crate::persistence::read_regular_file_bounded(
                            &file,
                            FILE_WRITE_MAX_BYTES as u64,
                        )?,
                    )?),
                    Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(String::new()),
                    Err(error) => Err(error.into()),
                }
            })();
            Some(match before {
                Ok(before) => protection.check_changes([(
                    path.as_path(),
                    before.as_str(),
                    args.content.as_str(),
                )]),
                Err(error) => protection.unavailable(error),
            })
        } else {
            None
        };
        cancellation.check()?;
        if let Some(result) = prose_check
            .as_ref()
            .and_then(|check| check.blocked_result(tool_name::WRITE))
        {
            return Ok(super::dispatch::FilesystemOutcome {
                result,
                paths: Vec::new(),
            });
        }
        if let Some(parent) = path.parent() {
            cancellation.check()?;
            fs::create_dir_all(parent)?;
            self.ensure_parent_inside_after_creation(
                parent,
                self.settings.absolute_paths && PathBuf::from(&args.path).is_absolute(),
            )?;
        }
        cancellation.check()?;
        match write(&path, args.content.as_bytes()) {
            Ok(()) => {}
            Err(error)
                if error
                    .downcast_ref::<crate::persistence::AtomicWriteCommittedButUndurable>()
                    .is_some() =>
            {
                self.mutation.record_file_snapshot(
                    SnapshotTool::WriteFile,
                    &path,
                    pre_snapshot.as_deref(),
                    Some(args.content.as_bytes()),
                );
                return Ok(super::dispatch::FilesystemOutcome {
                    result: ToolResult {
                        tool_name: tool_name::WRITE.to_string(),
                        success: false,
                        content: error.to_string(),
                        metadata: json!({
                            (meta::PATH): path,
                            (meta::BYTES): args.content.len(),
                            "outcome": "committed_but_undurable",
                        }),
                        display: ToolResultDisplay::default(),
                    },
                    paths: vec![path],
                });
            }
            Err(error) => return Err(error),
        }
        self.mutation.record_file_snapshot(
            SnapshotTool::WriteFile,
            &path,
            pre_snapshot.as_deref(),
            Some(args.content.as_bytes()),
        );
        let mut outcome = super::dispatch::FilesystemOutcome {
            result: ToolResult {
                tool_name: tool_name::WRITE.to_string(),
                success: true,
                content: format!("wrote {} bytes", args.content.len()),
                metadata: json!({(meta::PATH): path, (meta::BYTES): args.content.len()}),
                display: ToolResultDisplay::default(),
            },
            paths: vec![path],
        };
        if let Some(check) = prose_check {
            outcome.result.metadata["humanize_protection"] = check.metadata;
        }
        Ok(outcome)
    }
}