Skip to main content

defect_agent/session/
goal.rs

1//! Shared state for the goal-driven loop.
2//!
3//! In `--goal` mode, the agent runs autonomously for multiple turns until the goal is
4//! reached. The mechanism:
5//! - `goal_done` tool ([`crate::tool::GoalDoneTool`]) — called when the AI believes the
6//!   goal is reached, sets [`GoalState::reached`].
7//! - `goal-gate` hook ([`crate::hooks::builtin::GoalGate`]) — when a turn voluntarily
8//!   stops (`before_turn_end`), reads [`GoalState::is_reached`]: if reached, allows the
9//!   loop to end; otherwise, extends the turn (injects a "continue working" feedback) and
10//!   loops back for another round.
11//!
12//! Both share the same `Arc<GoalState>` across turn phases: the tool writes in one turn,
13//! the hook reads in a later turn.
14//!
15//! ## Why a named struct instead of a generic state bag
16//!
17//! Following the existing pattern in [`crate::session::DefaultSession`] (where
18//! `background`, `compaction_slot`, etc. are all purpose-specific named structs, not a
19//! catch-all `HashMap<String, Value>`). It currently has only two fields, but it's a
20//! struct —
21//! future additions like `summary`, `reached_at`, sub-goal lists, etc. can be added as
22//! fields. Since `ToolContext` and builtins hold an `Arc<GoalState>`, adding or removing
23//! fields does not break the interface.
24
25use std::sync::atomic::{AtomicBool, Ordering};
26
27/// Shared state for one goal-driven loop.
28#[derive(Debug)]
29pub struct GoalState {
30    /// The objective description passed via `--goal`. Injected into the `goal-gate`
31    /// keepalive feedback so the model sees the goal each round.
32    objective: String,
33    /// Whether the goal has been reached. Set by the `goal_done` tool; read by the
34    /// `goal-gate` hook.
35    reached: AtomicBool,
36}
37
38impl GoalState {
39    #[must_use]
40    pub fn new(objective: impl Into<String>) -> Self {
41        Self {
42            objective: objective.into(),
43            reached: AtomicBool::new(false),
44        }
45    }
46
47    /// The objective description.
48    #[must_use]
49    pub fn objective(&self) -> &str {
50        &self.objective
51    }
52
53    /// Mark the objective as reached (via the `goal_done` tool call).
54    pub fn mark_reached(&self) {
55        self.reached.store(true, Ordering::SeqCst);
56    }
57
58    /// Whether the goal has been reached (for `goal-gate` hook evaluation).
59    #[must_use]
60    pub fn is_reached(&self) -> bool {
61        self.reached.load(Ordering::SeqCst)
62    }
63}