aidens-cli 0.1.0

AiDENs CLI — agent creation, doctor diagnostics, scaffold generation, receipt inspection
Documentation
use super::*;

pub(crate) struct PlanConfigLoadOutcome {
    pub(crate) config_status: String,
    pub(crate) config: AiDENsConfigV1,
}

pub(crate) fn load_plan_config_file(path: &str) -> Result<PlanConfigLoadOutcome> {
    match load_config_file(path) {
        Ok(loaded) => Ok(PlanConfigLoadOutcome {
            config_status: format!("loaded {}", loaded.path.display()),
            config: loaded.config,
        }),
        Err(config_error) => load_test_agent_plan_config(path)
            .with_context(|| format!("failed to load {path} as AiDENs config ({config_error})")),
    }
}

pub(crate) fn load_test_agent_plan_config(path: &str) -> Result<PlanConfigLoadOutcome> {
    let config_path = resolve_cli_path(path)?;
    let test_agent = load_test_agent_file(&config_path)?;
    let reference_root = test_agent_reference_root(&config_path)?;
    let agent_segment = receipt_store_segment(&test_agent.agent.name);
    let sandbox_root = test_agent
        .tools
        .sandbox_root
        .as_ref()
        .map(|root| resolve_against_root(root, &reference_root))
        .unwrap_or_else(|| {
            reference_root
                .join("target/p21/plan-sandbox")
                .join(&agent_segment)
        });
    let receipt_root = test_agent
        .receipts
        .store_root
        .as_ref()
        .map(|root| resolve_against_root(root, &reference_root))
        .unwrap_or_else(|| {
            reference_root
                .join("target/p21/plan-receipts")
                .join(&agent_segment)
        });
    let (config, fixture_path, _) = test_agent_effective_config(
        &config_path,
        &test_agent,
        &sandbox_root,
        &receipt_root,
        false,
    )?;
    Ok(PlanConfigLoadOutcome {
        config_status: format!(
            "loaded test-agent {} via fixture {}",
            config_path.display(),
            fixture_path.display()
        ),
        config,
    })
}

pub(crate) fn load_or_default_config(path: &str) -> Result<(String, AiDENsConfigV1)> {
    let path_ref = Path::new(path);
    if path_ref.exists() {
        let loaded = load_config_file(path_ref)?;
        Ok((format!("loaded {}", loaded.path.display()), loaded.config))
    } else {
        Ok((
            format!(
                "missing {}; using safe disabled defaults",
                path_ref.display()
            ),
            AiDENsConfigV1::safe_default("aidens-cli"),
        ))
    }
}

pub(crate) fn receipt_store_config_from_options(
    store: Option<String>,
    config: Option<String>,
) -> Result<CanonicalEventLogConfig> {
    if let Some(root) = store {
        return Ok(CanonicalEventLogConfig::for_root(root));
    }
    if let Some(config) = config {
        let loaded = load_config_file(&config)?;
        let root =
            receipt_store_root_for_config(&loaded.config, &loaded.path).ok_or_else(|| {
                anyhow::anyhow!(
                    "receipt-store-not-configured: receipt_level=minimal and no --store override"
                )
            })?;
        return Ok(CanonicalEventLogConfig::for_root(root));
    }
    Ok(CanonicalEventLogConfig::for_root(
        "target/aidens-receipts/aidens-cli",
    ))
}

pub(crate) fn receipt_store_root_for_config(
    cfg: &AiDENsConfigV1,
    config_path: &Path,
) -> Option<String> {
    let Some(root) = cfg.receipts.store_root.clone() else {
        return default_receipt_store_root(&cfg.app_id, &cfg.receipt_level);
    };
    let path = PathBuf::from(root);
    if path.is_absolute() {
        return Some(path.display().to_string());
    }
    Some(
        config_path
            .parent()
            .unwrap_or_else(|| Path::new("."))
            .join(path)
            .display()
            .to_string(),
    )
}

pub(crate) fn memory_store_root_for_config(
    cfg: &AiDENsConfigV1,
    config_path: &Path,
) -> Option<String> {
    let root = cfg.memory.store_root.clone()?;
    let path = PathBuf::from(root);
    if path.is_absolute() {
        return Some(path.display().to_string());
    }
    Some(
        config_path
            .parent()
            .unwrap_or_else(|| Path::new("."))
            .join(path)
            .display()
            .to_string(),
    )
}

pub(crate) fn default_receipt_store_root(
    app_id: &str,
    receipt_level: &ReportLevelV1,
) -> Option<String> {
    if receipt_level == &ReportLevelV1::Minimal {
        return None;
    }
    Some(format!(
        "target/aidens-receipts/{}",
        receipt_store_segment(app_id)
    ))
}

pub(crate) fn receipt_store_segment(app_id: &str) -> String {
    let mut segment = String::new();
    let mut last_dash = false;
    for ch in app_id.trim().chars() {
        let next = if ch.is_ascii_alphanumeric() || ch == '_' {
            ch.to_ascii_lowercase()
        } else {
            '-'
        };
        if next == '-' {
            if !last_dash {
                segment.push(next);
            }
            last_dash = true;
        } else {
            segment.push(next);
            last_dash = false;
        }
    }
    let segment = segment.trim_matches('-').to_string();
    if segment.is_empty() {
        "aidens-cli".into()
    } else {
        segment
    }
}

pub(crate) fn plan_from_config(cfg: &AiDENsConfigV1) -> Result<AiDENsAppPlanV1> {
    let profile = profile_for_config(cfg)?;
    let mut input = ExecutionPlanAssemblyInputV1::from(profile.expand(cfg.app_id.clone())?);
    input.memory_mode = cfg.memory_mode.clone();
    input.receipt_level = cfg.receipt_level.clone();
    input.enabled_tool_bundles = cfg.tools.enabled_bundles.clone();
    Ok(assemble_execution_plan(input)
        .map_err(anyhow::Error::msg)?
        .plan)
}

pub(crate) fn compile_config_plan(
    config_status: &str,
    cfg: &AiDENsConfigV1,
) -> Result<AiDENsCompiledPlanV1> {
    let plan = plan_from_config(cfg)?;
    plan.validate().map_err(anyhow::Error::msg)?;
    validate_plan_runtime_contract(&plan, cfg)?;
    let provider_route = route_for_config(cfg);
    let tool_exposure = tool_exposure_for_config(cfg);
    let doctor = doctor_report_for_config(config_status, cfg);
    let config_apply_receipt = ConfigApplyReportV1::new(ConfigApplyReportDraftV1 {
        app_id: cfg.app_id.clone(),
        config_source: config_status.into(),
        provider_route: provider_route.clone(),
        tool_exposure: tool_exposure.clone(),
        memory_mode: plan.memory_mode.clone(),
        receipt_level: plan.receipt_level.clone(),
        enabled_tool_bundles: cfg.tools.enabled_bundles.clone(),
        sandbox_root: cfg.tools.sandbox_root.clone(),
        applied: true,
        reason_codes: vec!["plan-kit:execution-plan-assembly-only".into()],
    });
    let parity_report = plan_runtime_parity_report(&plan, &provider_route, &tool_exposure, &doctor);
    if !parity_report.is_passing() {
        bail!(
            "plan/runtime parity failed: {}",
            parity_report.mismatches.join("; ")
        );
    }
    Ok(AiDENsCompiledPlanV1 {
        plan_id: ArtifactId::new("compiled-plan"),
        plan,
        provider_route,
        tool_exposure,
        doctor,
        config_apply_receipt,
        parity_report,
    })
}

pub(crate) fn validate_plan_runtime_contract(
    plan: &AiDENsAppPlanV1,
    cfg: &AiDENsConfigV1,
) -> Result<()> {
    if plan.provider_required && is_disabled_provider(&cfg.provider.kind) {
        bail!("provider_required plan cannot treat disabled provider as valid")
    }
    let readiness = provider_readiness_for_spec(&provider_spec_from_config(&cfg.provider));
    if plan.provider_required && !readiness.executable {
        bail!(
            "provider_required plan has no executable provider: {}",
            readiness.reason_codes.join(",")
        )
    }
    ensure_memory_store_policy(cfg)?;
    Ok(())
}

pub(crate) fn profile_for_config(cfg: &AiDENsConfigV1) -> Result<AiDENsProfile> {
    match cfg.profile_id.as_deref() {
        Some(profile_id) => AiDENsProfile::from_id(profile_id)
            .ok_or_else(|| anyhow::anyhow!("unknown AiDENs profile: {profile_id}")),
        None => Ok(AiDENsProfile::ChatOnly),
    }
}

pub(crate) fn ensure_profile_policy(cfg: &AiDENsConfigV1) -> Result<()> {
    profile_for_config(cfg).map(|_| ())
}

pub(crate) fn ensure_memory_store_policy(cfg: &AiDENsConfigV1) -> Result<()> {
    if cfg.memory_mode == MemoryModeV1::Required && cfg.memory.store_root.is_none() {
        bail!(
            "memory-required-without-durable-store: memory_mode=required needs [memory].store_root"
        )
    }
    Ok(())
}

pub(crate) fn route_for_config(cfg: &AiDENsConfigV1) -> ProviderRouteReportV1 {
    let spec = provider_spec_from_config(&cfg.provider);
    route_receipt_for_spec(&spec)
}

pub(crate) fn is_disabled_provider(kind: &str) -> bool {
    matches!(
        kind.trim().to_ascii_lowercase().as_str(),
        "" | "disabled" | "none"
    )
}

pub(crate) fn provider_spec_from_config(provider: &ProviderConfigV1) -> ProviderSpecV1 {
    ProviderSpecV1 {
        kind: provider.kind.clone(),
        model: provider.model.clone(),
        api_key: provider.api_key.clone(),
        base_url: provider.base_url.clone(),
        mock_response: provider.mock_response.clone(),
    }
}