Skip to main content

zeph_core/goal/
mod.rs

1// SPDX-FileCopyrightText: 2026 Andrei G <bug-ops>
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4//! Long-horizon goal lifecycle subsystem.
5//!
6//! A goal is a persistent user intent that spans multiple turns. At most one goal
7//! can be `Active` at a time. The subsystem tracks token consumption per turn and
8//! injects an `<active_goal>` block into the volatile system-prompt region.
9//!
10//! ## Architecture
11//!
12//! - [`GoalStatus`] — FSM states with valid transition table.
13//! - [`Goal`] / [`GoalSnapshot`] — full DB row vs. lightweight cross-crate view.
14//! - [`GoalStore`] — SQLite/Postgres-backed persistence with transactional `create()`.
15//! - [`GoalAccounting`] — per-turn token accounting service.
16//!
17//! ## Invariants
18//!
19//! - **G1**: At most one `Active` goal per database. Enforced jointly by the partial
20//!   unique index and the transactional `GoalStore::create`.
21//! - **G2**: Stale transitions return [`GoalError::StaleUpdate`]; handlers refetch silently.
22//! - **G3**: `<active_goal>` block appears only after `<!-- cache:volatile -->` in the
23//!   system prompt. Enforced by a snapshot test in `context/assembly.rs`.
24//! - **G4**: `GoalAccounting::on_turn_complete` is fire-and-forget. DB write failures
25//!   log a `WARN` and never abort the turn.
26
27mod accounting;
28pub mod autonomous;
29pub mod registry;
30mod state;
31pub mod store;
32pub mod supervisor;
33
34pub use accounting::GoalAccounting;
35pub use autonomous::{AutonomousDriver, AutonomousSession, AutonomousState, SupervisorVerdict};
36pub use registry::AutonomousRegistry;
37pub use state::GoalStatus;
38pub use store::{GoalError, GoalStore};
39pub use supervisor::GoalSupervisor;
40
41use chrono::{DateTime, Utc};
42
43/// A persisted long-horizon goal row.
44#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
45pub struct Goal {
46    /// Unique identifier (UUID v4 string).
47    pub id: String,
48    /// User-provided goal text (max `[goals] max_text_chars` chars at creation time).
49    pub text: String,
50    /// Current FSM status.
51    pub status: GoalStatus,
52    /// Optional token budget. `None` means unlimited.
53    pub token_budget: Option<i64>,
54    /// Number of conversation turns completed under this goal.
55    pub turns_used: i64,
56    /// Total tokens consumed across all turns under this goal.
57    pub tokens_used: i64,
58    /// When the goal was created.
59    pub created_at: DateTime<Utc>,
60    /// Last mutation time; used as a CAS guard for stale-update detection.
61    pub updated_at: DateTime<Utc>,
62    /// When the goal reached `Completed` or `Cleared`. `None` while active or paused.
63    pub completed_at: Option<DateTime<Utc>>,
64}
65
66/// Lightweight, cross-crate snapshot of an active goal.
67///
68/// Carries only what TUI and command handlers need, without pulling `zeph-core` into `zeph-tui`.
69#[derive(Debug, Clone, serde::Serialize)]
70pub struct GoalSnapshot {
71    /// UUID string of the goal.
72    pub id: String,
73    /// Goal text, pre-validated to fit within `max_text_chars`.
74    pub text: String,
75    /// Current FSM status.
76    pub status: GoalStatus,
77    /// Number of turns completed.
78    pub turns_used: u64,
79    /// Total tokens consumed.
80    pub tokens_used: u64,
81    /// Optional token budget (`None` = unlimited).
82    pub token_budget: Option<u64>,
83}
84
85impl From<Goal> for GoalSnapshot {
86    fn from(g: Goal) -> Self {
87        Self {
88            id: g.id,
89            text: g.text,
90            status: g.status,
91            turns_used: g.turns_used.max(0).cast_unsigned(),
92            tokens_used: g.tokens_used.max(0).cast_unsigned(),
93            token_budget: g.token_budget.map(|b| b.max(0).cast_unsigned()),
94        }
95    }
96}