carryctx 0.0.2

Persistent project context for coding agents
use crate::adapter::sqlite_repos::{SqliteAgentRepository, SqliteEventRepository};
use crate::adapter::unit_of_work::UnitOfWork;
use crate::domain::agent::{Agent, AgentStatus, validate_agent_name};
use crate::error::CarryCtxError;
use crate::repository::agent::{AgentFilter, AgentRepository, NewAgent};
use crate::repository::event::{EventRepository, NewEvent};

fn now() -> String {
    chrono::Utc::now().to_rfc3339()
}

fn new_id() -> String {
    ulid::Ulid::new().to_string()
}

fn resolve_agent(
    project_id: &str,
    ref_: &str,
    repo: &SqliteAgentRepository,
) -> Result<Agent, CarryCtxError> {
    let by_name = repo.find_by_name(project_id, ref_)?;
    if let Some(agent) = by_name {
        return Ok(agent);
    }
    repo.find_by_id(project_id, ref_)?
        .ok_or_else(|| CarryCtxError::resource_not_found(format!("Agent '{ref_}' not found.")))
}

pub fn register_agent(
    project_id: &str,
    name: &str,
    provider: Option<&str>,
    metadata: serde_json::Value,
    uow: &UnitOfWork,
) -> Result<Agent, CarryCtxError> {
    validate_agent_name(name)?;
    let now = now();
    let agent_id = new_id();
    let provider = provider.unwrap_or("custom");

    let conn = uow.connection();
    let agent_repo = SqliteAgentRepository::new(conn);
    let event_repo = SqliteEventRepository::new(conn);

    let agent = agent_repo.register(
        &NewAgent {
            id: agent_id.clone(),
            project_id: project_id.to_string(),
            name: name.to_string(),
            provider: provider.to_string(),
            role: None,
            metadata: if metadata.is_object() {
                let mut m = serde_json::json!({ "provider": provider });
                if let Some(obj) = metadata.as_object() {
                    for (k, v) in obj {
                        m[k] = v.clone();
                    }
                }
                m
            } else {
                serde_json::json!({ "provider": provider })
            },
        },
        &now,
    )?;

    event_repo.append(&NewEvent {
        id: new_id(),
        project_id: project_id.to_string(),
        event_type: "agent.registered".into(),
        actor_agent_id: Some(agent_id.clone()),
        session_id: None,
        task_id: None,
        payload: serde_json::json!({
            "id": agent.id,
            "name": agent.name,
            "metadata": agent.metadata,
        }),
        occurred_at: now,
    })?;

    Ok(agent)
}

pub fn list_agents(
    _project_id: &str,
    filter: &AgentFilter,
    uow: &UnitOfWork,
) -> Result<Vec<Agent>, CarryCtxError> {
    let conn = uow.connection();
    let repo = SqliteAgentRepository::new(conn);
    repo.list(filter)
}

pub fn show_agent(project_id: &str, ref_: &str, uow: &UnitOfWork) -> Result<Agent, CarryCtxError> {
    let conn = uow.connection();
    let repo = SqliteAgentRepository::new(conn);
    resolve_agent(project_id, ref_, &repo)
}

pub fn rename_agent(
    project_id: &str,
    target_ref: &str,
    new_name: &str,
    uow: &UnitOfWork,
) -> Result<Agent, CarryCtxError> {
    validate_agent_name(new_name)?;
    let now = now();

    let conn = uow.connection();
    let agent_repo = SqliteAgentRepository::new(conn);
    let event_repo = SqliteEventRepository::new(conn);

    let existing = resolve_agent(project_id, target_ref, &agent_repo)?;

    let updated = agent_repo.rename(&existing.id, project_id, new_name, &now)?;

    event_repo.append(&NewEvent {
        id: new_id(),
        project_id: project_id.to_string(),
        event_type: "agent.renamed".into(),
        actor_agent_id: Some(existing.id.clone()),
        session_id: None,
        task_id: None,
        payload: serde_json::json!({
            "id": existing.id,
            "beforeName": existing.name,
            "afterName": new_name,
        }),
        occurred_at: now,
    })?;

    Ok(updated)
}

pub fn deactivate_agent(
    project_id: &str,
    target_ref: &str,
    uow: &UnitOfWork,
) -> Result<Agent, CarryCtxError> {
    let now = now();

    let conn = uow.connection();
    let agent_repo = SqliteAgentRepository::new(conn);
    let event_repo = SqliteEventRepository::new(conn);

    let existing = resolve_agent(project_id, target_ref, &agent_repo)?;

    if existing.status == AgentStatus::Deactivated {
        return Err(CarryCtxError::state_conflict(format!(
            "Agent '{}' is already deactivated.",
            existing.name
        )));
    }

    let updated = agent_repo.deactivate(&existing.id, project_id, &now)?;

    event_repo.append(&NewEvent {
        id: new_id(),
        project_id: project_id.to_string(),
        event_type: "agent.deactivated".into(),
        actor_agent_id: Some(existing.id.clone()),
        session_id: None,
        task_id: None,
        payload: serde_json::json!({
            "id": existing.id,
            "name": existing.name,
        }),
        occurred_at: now,
    })?;

    Ok(updated)
}