a3s 0.10.1

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

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

#[derive(Debug, Clone, Default, PartialEq)]
pub(super) struct CodeWebContextUsage {
    pub(super) estimated_tokens: usize,
    pub(super) limit_tokens: u32,
    pub(super) history_messages: usize,
    pub(super) compacted: bool,
    pub(super) compact_summary: Option<String>,
}

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,
    settings: &CodeWebSessionSettings,
    context: &CodeWebContextUsage,
    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;
    let goal_active = controls.goal.is_some();
    json!({
        "sessionId": session_id,
        "effort": profile.id,
        "goal": controls.goal.clone(),
        "goalState": controls.goal_run.clone(),
        "planningMode": if goal_active {
            "enabled"
        } else {
            settings.planning_mode.as_deref().unwrap_or("auto")
        },
        "goalTracking": goal_active || settings.goal_tracking.unwrap_or(false),
        "context": {
            "estimatedTokens": context.estimated_tokens,
            "limitTokens": context.limit_tokens,
            "percent": context_percent(context.estimated_tokens, context.limit_tokens),
            "historyMessages": context.history_messages,
            "compacted": context.compacted,
            "compactSummary": context.compact_summary.clone(),
        },
        "effortLevel": budget::effort_profile_json(profile),
        "effortLevels": effort_levels_json(),
        "budget": budget::budget_plan_json(&plan),
    })
}

fn context_percent(estimated_tokens: usize, limit_tokens: u32) -> f64 {
    if limit_tokens == 0 {
        return 0.0;
    }
    (estimated_tokens as f64 / f64::from(limit_tokens)).clamp(0.0, 1.0)
}

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()),
            goal_run: None,
        };
        let settings = CodeWebSessionSettings {
            planning_mode: Some("disabled".to_string()),
            goal_tracking: Some(false),
            ..CodeWebSessionSettings::default()
        };
        let context = CodeWebContextUsage {
            estimated_tokens: 8_192,
            limit_tokens: 32_768,
            history_messages: 12,
            compacted: true,
            compact_summary: Some("Earlier work was compacted".to_string()),
        };
        let value = controls_json(
            "session-1",
            &controls,
            &settings,
            &context,
            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["planningMode"], "enabled");
        assert_eq!(value["goalTracking"], true);
        assert_eq!(value["context"]["estimatedTokens"], 8_192);
        assert_eq!(value["context"]["limitTokens"], 32_768);
        assert_eq!(value["context"]["percent"], 0.25);
        assert_eq!(value["context"]["historyMessages"], 12);
        assert_eq!(value["context"]["compacted"], true);
        assert_eq!(
            value["context"]["compactSummary"],
            "Earlier work was compacted"
        );
        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,
            goal_run: None,
        };
        let value = controls_json(
            "session-2",
            &controls,
            &CodeWebSessionSettings::default(),
            &CodeWebContextUsage::default(),
            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["planningMode"], "auto");
        assert_eq!(value["goalTracking"], false);
        assert_eq!(value["context"]["percent"], 0.0);
        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)
        );
    }
}