holon 0.14.1

A headless, event-driven runtime for long-lived agents
Documentation
use std::{
    fs,
    fs::File,
    io::Read,
    path::{Path, PathBuf},
    time::SystemTime,
};

use anyhow::{Context, Result};
use chrono::{DateTime, Utc};
use sha2::{Digest, Sha256};

use crate::types::{
    agent_home_workspace_id, WorkItemPlanArtifact, WorkItemRecord, AGENT_HOME_WORKSPACE_ID,
};

const PLAN_PREVIEW_CHARS: usize = 1600;

pub(crate) fn plan_path(agent_home: &Path, work_item_id: &str) -> PathBuf {
    agent_home
        .join("work-items")
        .join(work_item_id)
        .join("plan.md")
}

pub(crate) fn plan_relative_path(work_item_id: &str) -> PathBuf {
    PathBuf::from("work-items")
        .join(work_item_id)
        .join("plan.md")
}

pub(crate) fn ensure_plan_artifact(
    agent_home: &Path,
    record: &WorkItemRecord,
    initial_plan: Option<&str>,
) -> Result<WorkItemPlanArtifact> {
    let path = plan_path(agent_home, &record.id);
    if !path.exists() {
        if let Some(parent) = path.parent() {
            fs::create_dir_all(parent)
                .with_context(|| format!("failed to create {}", parent.display()))?;
        }
        let body = initial_plan
            .or(record.plan.as_deref())
            .unwrap_or_default()
            .as_bytes();
        fs::write(&path, body).with_context(|| format!("failed to write {}", path.display()))?;
    }
    describe_plan_artifact(&path, &record.agent_id, &record.id)
}

pub(crate) fn refresh_plan_artifact_metadata(
    agent_home: &Path,
    record: &mut WorkItemRecord,
) -> Result<bool> {
    let previous = record.plan_artifact.clone();
    let had_inline_plan = record.plan.is_some();
    let path = plan_path(agent_home, &record.id);
    if !path.exists() && record.plan.is_none() && record.plan_artifact.is_some() {
        anyhow::bail!(
            "missing plan artifact {} for work item {}",
            path.display(),
            record.id
        );
    }
    let artifact = ensure_plan_artifact(agent_home, record, None)?;
    record.plan = None;
    record.plan_artifact = Some(artifact);
    Ok(had_inline_plan || record.plan_artifact != previous)
}

pub(crate) fn describe_plan_artifact(
    path: &Path,
    owner_agent_id: &str,
    work_item_id: &str,
) -> Result<WorkItemPlanArtifact> {
    let mut file =
        File::open(path).with_context(|| format!("failed to open {}", path.display()))?;
    let metadata = file
        .metadata()
        .with_context(|| format!("failed to stat {}", path.display()))?;
    let mut content = Vec::new();
    file.read_to_end(&mut content)
        .with_context(|| format!("failed to read {}", path.display()))?;
    let hash = format!("sha256:{:x}", Sha256::digest(&content));
    let text = String::from_utf8_lossy(&content);
    let mut chars = text.chars();
    let preview = chars.by_ref().take(PLAN_PREVIEW_CHARS).collect::<String>();
    let preview_complete = chars.next().is_none();
    let updated_at = metadata
        .modified()
        .ok()
        .map(DateTime::<Utc>::from)
        .unwrap_or_else(|| DateTime::<Utc>::from(SystemTime::UNIX_EPOCH));
    Ok(WorkItemPlanArtifact {
        owner_agent_id: owner_agent_id.to_string(),
        workspace_id: agent_home_workspace_id(owner_agent_id),
        workspace_alias: Some(AGENT_HOME_WORKSPACE_ID.into()),
        relative_path: plan_relative_path(work_item_id),
        path: path.to_path_buf(),
        hash,
        bytes: metadata.len(),
        updated_at,
        preview,
        preview_complete,
    })
}