a3s 0.10.0

a3s — A3S coding agent CLI; `a3s code` launches the interactive TUI
Documentation
use super::*;

impl KernelService {
    pub(in crate::api::code_web) async fn update_goal_action(
        &self,
        session_id: &str,
        action: &str,
    ) -> BootResult<Value> {
        self.kernel_session(session_id).await?;
        let goal = {
            let mut controls_by_session = self.state.session_controls.lock().await;
            let controls = controls_by_session
                .entry(session_id.to_string())
                .or_default();
            let goal = controls.goal.clone().ok_or_else(|| {
                BootError::BadRequest("the session has no active goal".to_string())
            })?;
            let run = controls.goal_run.get_or_insert_with(|| {
                let now = chrono::Utc::now().timestamp_millis();
                CodeWebGoalRun {
                    started_at: now,
                    updated_at: now,
                    ..CodeWebGoalRun::default()
                }
            });
            let now = chrono::Utc::now().timestamp_millis();
            match action {
                "pause" => run.status = CodeWebGoalStatus::Paused,
                "resume" => {
                    if run.status == CodeWebGoalStatus::Achieved {
                        return Err(BootError::BadRequest(
                            "an achieved goal cannot be resumed; set a new goal instead"
                                .to_string(),
                        ));
                    }
                    run.status = CodeWebGoalStatus::Active;
                    run.completed_at = None;
                }
                "retry" => {
                    if run.status == CodeWebGoalStatus::Achieved {
                        return Err(BootError::BadRequest(
                            "an achieved goal cannot be retried; set a new goal instead"
                                .to_string(),
                        ));
                    }
                    run.status = CodeWebGoalStatus::Retrying;
                    run.completed_at = None;
                    run.last_error = None;
                }
                _ => {
                    return Err(BootError::BadRequest(format!(
                        "unsupported goal action `{action}`"
                    )))
                }
            }
            run.updated_at = now;
            goal
        };

        if action == "pause" {
            self.state
                .session_turn_queues
                .lock()
                .await
                .entry(session_id.to_string())
                .or_default()
                .remove_kind(CodeWebQueuedTurnKind::GoalContinuation);
        } else {
            self.state
                .session_turn_queues
                .lock()
                .await
                .entry(session_id.to_string())
                .or_default()
                .resume();
            let attempt = self
                .session_controls_snapshot(session_id)
                .await
                .goal_run
                .map(|run| run.attempts)
                .unwrap_or_default();
            self.enqueue_goal_continuation(session_id, &goal, attempt)
                .await?;
        }
        self.persist_session_state(session_id).await?;
        self.session_controls(session_id).await
    }

    pub(in crate::api::code_web) async fn cancel_session(
        &self,
        session_id: &str,
    ) -> BootResult<Value> {
        let session = self.kernel_session(session_id).await?;
        let cancelled = session.cancel().await;
        {
            let mut controls_by_session = self.state.session_controls.lock().await;
            if let Some(run) = controls_by_session
                .entry(session_id.to_string())
                .or_default()
                .goal_run
                .as_mut()
            {
                if run.status != CodeWebGoalStatus::Achieved {
                    run.status = CodeWebGoalStatus::Paused;
                    run.updated_at = chrono::Utc::now().timestamp_millis();
                }
            }
        }
        {
            let mut queues = self.state.session_turn_queues.lock().await;
            let queue = queues.entry(session_id.to_string()).or_default();
            queue.pause();
            queue.remove_kind(CodeWebQueuedTurnKind::GoalContinuation);
        }
        self.persist_session_state(session_id).await?;
        Ok(json!({
            "sessionId": session_id,
            "cancelled": cancelled,
        }))
    }

    pub(super) async fn begin_goal_attempt(&self, session_id: &str) {
        let mut controls_by_session = self.state.session_controls.lock().await;
        let Some(run) = controls_by_session
            .entry(session_id.to_string())
            .or_default()
            .goal_run
            .as_mut()
        else {
            return;
        };
        if !matches!(
            run.status,
            CodeWebGoalStatus::Active | CodeWebGoalStatus::Retrying
        ) {
            return;
        }
        run.status = CodeWebGoalStatus::Active;
        run.attempts = run.attempts.saturating_add(1);
        run.progress_percent = 0;
        run.completed_steps = 0;
        run.total_steps = 0;
        run.extracted_goal = None;
        run.updated_at = chrono::Utc::now().timestamp_millis();
    }

    pub(super) async fn observe_goal_event(&self, session_id: &str, event: &AgentEvent) {
        let mut controls_by_session = self.state.session_controls.lock().await;
        let Some(run) = controls_by_session
            .entry(session_id.to_string())
            .or_default()
            .goal_run
            .as_mut()
        else {
            return;
        };
        if run.status == CodeWebGoalStatus::Paused || run.status == CodeWebGoalStatus::Achieved {
            return;
        }
        apply_goal_event(run, event, chrono::Utc::now().timestamp_millis());
    }

    pub(super) async fn pending_goal_continuation(
        &self,
        session_id: &str,
    ) -> Option<(String, u32)> {
        let controls = self.session_controls_snapshot(session_id).await;
        let goal = controls.goal?;
        let run = controls.goal_run?;
        matches!(
            run.status,
            CodeWebGoalStatus::Active | CodeWebGoalStatus::Retrying
        )
        .then_some((goal, run.attempts))
    }
}

fn apply_goal_event(run: &mut CodeWebGoalRun, event: &AgentEvent, now: i64) {
    match event {
        AgentEvent::GoalExtracted { goal } => {
            run.extracted_goal = normalize_goal(&goal.description);
            run.updated_at = now;
        }
        AgentEvent::GoalProgress {
            progress,
            completed_steps,
            total_steps,
            ..
        } => {
            run.progress_percent = (progress.clamp(0.0, 1.0) * 100.0).round() as u8;
            run.completed_steps = *completed_steps;
            run.total_steps = *total_steps;
            run.updated_at = now;
        }
        AgentEvent::GoalAchieved {
            goal, total_steps, ..
        } if run.extracted_goal.as_deref() == normalize_goal(goal).as_deref() => {
            run.status = CodeWebGoalStatus::Achieved;
            run.progress_percent = 100;
            run.completed_steps = *total_steps;
            run.total_steps = *total_steps;
            run.completed_at = Some(now);
            run.updated_at = now;
            run.last_error = None;
        }
        AgentEvent::Error { message } => {
            run.status = CodeWebGoalStatus::Retrying;
            run.last_error = Some(message.clone());
            run.updated_at = now;
        }
        _ => {}
    }
}

pub(super) fn goal_continuation_prompt(goal: &str, attempt: u32) -> String {
    format!(
        "The previous goal attempt ended without a matching GoalAchieved event. The goal remains active.\n\
         Goal: {goal}\n\
         Continue with attempt {}. Preserve verified work, identify the highest-value remaining gap, implement it, and independently verify the result. Do not claim completion without fresh evidence.",
        attempt.saturating_add(1)
    )
}

#[cfg(test)]
mod tests {
    use super::*;
    use a3s_code_core::planning::AgentGoal;

    #[test]
    fn only_matching_extracted_goal_closes_the_durable_goal() {
        let mut run = CodeWebGoalRun {
            status: CodeWebGoalStatus::Active,
            started_at: 1,
            updated_at: 1,
            ..CodeWebGoalRun::default()
        };
        apply_goal_event(
            &mut run,
            &AgentEvent::GoalExtracted {
                goal: AgentGoal::new("focused tests pass"),
            },
            2,
        );
        apply_goal_event(
            &mut run,
            &AgentEvent::GoalAchieved {
                goal: "different goal".to_string(),
                total_steps: 3,
                duration_ms: 10,
            },
            3,
        );
        assert_eq!(run.status, CodeWebGoalStatus::Active);

        apply_goal_event(
            &mut run,
            &AgentEvent::GoalAchieved {
                goal: "focused tests pass".to_string(),
                total_steps: 4,
                duration_ms: 20,
            },
            4,
        );
        assert_eq!(run.status, CodeWebGoalStatus::Achieved);
        assert_eq!(run.progress_percent, 100);
        assert_eq!(run.completed_at, Some(4));
    }

    #[test]
    fn continuation_prompt_keeps_verification_as_the_completion_gate() {
        let prompt = goal_continuation_prompt("ship the Web flow", 2);
        assert!(prompt.contains("attempt 3"));
        assert!(prompt.contains("GoalAchieved"));
        assert!(prompt.contains("fresh evidence"));
    }
}