agent-orchestrator-sdk 0.1.1

Rust SDK for orchestrating LLM-powered agents, shared task execution, and teammate coordination
Documentation
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
use std::path::{Path, PathBuf};

use crate::config::AGENT_DIR;
use crate::error::{SdkError, SdkResult};
use uuid::Uuid;

#[derive(Debug, Clone)]
pub struct AgentPaths {
    work_dir: PathBuf,
    home_dir: PathBuf,
    project_key: String,
}

impl AgentPaths {
    pub fn for_work_dir(work_dir: &Path) -> SdkResult<Self> {
        let work_dir = canonicalize_for_storage(work_dir)?;
        let home_dir = dirs::home_dir()
            .ok_or_else(|| SdkError::Config("Could not resolve home directory".to_string()))?
            .join(AGENT_DIR);
        let identity_path = git_common_dir(&work_dir).unwrap_or_else(|| work_dir.clone());
        let project_key = project_key_for_path(&identity_path);

        Ok(Self {
            work_dir,
            home_dir,
            project_key,
        })
    }

    pub fn project_config_dir(&self) -> PathBuf {
        self.work_dir.join(AGENT_DIR)
    }

    pub fn project_settings_path(&self) -> PathBuf {
        self.project_config_dir().join("settings.json")
    }

    pub fn project_local_settings_path(&self) -> PathBuf {
        self.project_config_dir().join("settings.local.json")
    }

    pub fn user_root_dir(&self) -> PathBuf {
        self.home_dir.clone()
    }

    pub fn user_settings_path(&self) -> PathBuf {
        self.home_dir.join("settings.json")
    }

    pub fn projects_dir(&self) -> PathBuf {
        self.home_dir.join("projects")
    }

    pub fn project_state_dir(&self) -> PathBuf {
        self.projects_dir().join(&self.project_key)
    }

    pub fn project_tasks_dir(&self) -> PathBuf {
        self.project_state_dir().join("tasks")
    }

    pub fn project_mailbox_dir(&self) -> PathBuf {
        self.project_state_dir().join("mailbox")
    }

    pub fn project_memory_dir(&self) -> PathBuf {
        self.project_state_dir().join("memory")
    }

    pub fn project_sessions_dir(&self) -> PathBuf {
        self.project_state_dir().join("sessions")
    }

    pub fn cli_session_path(&self) -> PathBuf {
        self.project_sessions_dir().join("cli-session.json")
    }

    pub fn project_key(&self) -> &str {
        &self.project_key
    }

    pub fn teams_dir(&self) -> PathBuf {
        self.home_dir.join("teams")
    }

    pub fn tasks_dir(&self) -> PathBuf {
        self.home_dir.join("tasks")
    }

    pub fn new_team_name(&self) -> String {
        format!("{}-{}", self.project_key, &Uuid::new_v4().to_string()[..8])
    }

    pub fn team_dir(&self, team_name: &str) -> PathBuf {
        self.teams_dir().join(team_name)
    }

    pub fn team_config_path(&self, team_name: &str) -> PathBuf {
        self.team_dir(team_name).join("config.json")
    }

    pub fn team_mailbox_dir(&self, team_name: &str) -> PathBuf {
        self.team_dir(team_name).join("mailbox")
    }

    pub fn team_memory_dir(&self, team_name: &str) -> PathBuf {
        self.team_dir(team_name).join("memory")
    }

    pub fn team_tasks_dir(&self, team_name: &str) -> PathBuf {
        self.tasks_dir().join(team_name)
    }
}

fn canonicalize_for_storage(path: &Path) -> SdkResult<PathBuf> {
    if path.exists() {
        return std::fs::canonicalize(path).map_err(SdkError::Io);
    }

    let joined = std::env::current_dir().map_err(SdkError::Io)?.join(path);
    Ok(joined)
}

fn project_key_for_path(path: &Path) -> String {
    let label = path
        .file_name()
        .and_then(|s| s.to_str())
        .filter(|s| !s.is_empty())
        .unwrap_or("project");
    let slug: String = label
        .chars()
        .map(|c| {
            if c.is_ascii_alphanumeric() || c == '-' || c == '_' {
                c
            } else {
                '-'
            }
        })
        .collect();

    let mut hasher = DefaultHasher::new();
    path.hash(&mut hasher);
    let hash = hasher.finish();

    format!("{slug}-{hash:016x}")
}

fn git_common_dir(work_dir: &Path) -> Option<PathBuf> {
    for dir in work_dir.ancestors() {
        let git_entry = dir.join(".git");

        if git_entry.is_dir() {
            return std::fs::canonicalize(git_entry).ok();
        }

        if git_entry.is_file() {
            let gitdir = resolve_gitdir_from_file(dir, &git_entry)?;
            let common = resolve_common_dir(&gitdir).unwrap_or(gitdir);
            return std::fs::canonicalize(common).ok();
        }
    }

    None
}

fn resolve_gitdir_from_file(base_dir: &Path, git_file: &Path) -> Option<PathBuf> {
    let content = std::fs::read_to_string(git_file).ok()?;
    let gitdir = content.trim().strip_prefix("gitdir:")?.trim();
    let path = Path::new(gitdir);
    let resolved = if path.is_absolute() {
        path.to_path_buf()
    } else {
        base_dir.join(path)
    };
    Some(resolved)
}

fn resolve_common_dir(gitdir: &Path) -> Option<PathBuf> {
    let commondir = gitdir.join("commondir");
    let content = std::fs::read_to_string(commondir).ok()?;
    let path = Path::new(content.trim());
    Some(if path.is_absolute() {
        path.to_path_buf()
    } else {
        gitdir.join(path)
    })
}