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}