a3s 0.9.5

a3s — A3S coding agent CLI; `a3s code` launches the interactive TUI
Documentation
use serde_json::{json, Value};

use crate::api::code_web::state::CodeWebSessionControls;
use crate::budget::{self, BudgetProfile, BudgetWorkload};

pub(super) fn effort_levels_json() -> Vec<Value> {
    budget::effort_levels_json()
}

pub(super) fn normalize_effort(value: &str) -> Option<&'static BudgetProfile> {
    budget::normalize_effort(value)
}

pub(super) fn controls_json(
    session_id: &str,
    controls: &CodeWebSessionControls,
    context_limit: Option<u32>,
    auto_compact_threshold: f64,
) -> Value {
    let profile = normalize_effort(&controls.effort)
        .or_else(|| normalize_effort(budget::DEFAULT_CODE_WEB_EFFORT_ID))
        .expect("medium effort profile must exist");
    let mut plan =
        budget::budget_plan_for_profile(profile, context_limit, BudgetWorkload::Interactive);
    plan.auto_compact_threshold = auto_compact_threshold;
    json!({
        "sessionId": session_id,
        "effort": profile.id,
        "goal": controls.goal.clone(),
        "effortLevel": budget::effort_profile_json(profile),
        "effortLevels": effort_levels_json(),
        "budget": budget::budget_plan_json(&plan),
    })
}

pub(super) fn normalize_goal(value: &str) -> Option<String> {
    let normalized = value.split_whitespace().collect::<Vec<_>>().join(" ");
    if normalized.is_empty() {
        None
    } else {
        Some(normalized)
    }
}

pub(super) fn compose_controlled_prompt(
    controls: &CodeWebSessionControls,
    user_prompt: &str,
) -> String {
    if user_prompt.trim_start().starts_with('/') {
        return user_prompt.to_string();
    }

    let mut directives = Vec::new();
    if let Some(profile) = normalize_effort(&controls.effort) {
        if let Some(guideline) = profile.guideline {
            directives.push(guideline.to_string());
        }
    }
    if let Some(goal) = controls.goal.as_deref().and_then(normalize_goal) {
        directives.push(format!(
            "[session goal] Keep this north-star goal in mind for this turn: {goal}"
        ));
    }

    if directives.is_empty() {
        user_prompt.to_string()
    } else {
        format!(
            "{}\n\nUser request:\n{}",
            directives.join("\n\n"),
            user_prompt
        )
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::budget::{BudgetWorkload, DEFAULT_CODE_WEB_EFFORT_ID};

    #[test]
    fn controls_json_exposes_budget_for_selected_effort() {
        let controls = CodeWebSessionControls {
            effort: "xhigh".to_string(),
            goal: Some("finish the migration".to_string()),
        };
        let value = controls_json("session-1", &controls, Some(32_768), 0.9);
        let budget =
            budget::budget_plan_for_effort_id("xhigh", Some(32_768), BudgetWorkload::Interactive);

        assert_eq!(value["sessionId"], "session-1");
        assert_eq!(value["effort"], "xhigh");
        assert_eq!(value["goal"], "finish the migration");
        assert_eq!(
            value["budget"]["maxToolRounds"].as_u64(),
            Some(budget.max_tool_rounds as u64)
        );
        assert_eq!(
            value["budget"]["maxParallelTasks"].as_u64(),
            Some(budget.max_parallel_tasks as u64)
        );
        assert_eq!(
            value["budget"]["workflowMaxToolCalls"].as_u64(),
            Some(budget.workflow_max_tool_calls as u64)
        );
        assert_eq!(value["budget"]["autoCompactThreshold"].as_f64(), Some(0.9));
    }

    #[test]
    fn controls_json_falls_back_to_default_budget_for_unknown_effort() {
        let controls = CodeWebSessionControls {
            effort: "warp-drive".to_string(),
            goal: None,
        };
        let value = controls_json("session-2", &controls, None, 0.85);
        let budget = budget::budget_plan_for_effort_id(
            DEFAULT_CODE_WEB_EFFORT_ID,
            None,
            BudgetWorkload::Interactive,
        );

        assert_eq!(value["effort"], DEFAULT_CODE_WEB_EFFORT_ID);
        assert_eq!(
            value["budget"]["maxToolRounds"].as_u64(),
            Some(budget.max_tool_rounds as u64)
        );
        assert_eq!(
            value["budget"]["maxParallelTasks"].as_u64(),
            Some(budget.max_parallel_tasks as u64)
        );
    }
}