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;
28mod state;
29pub mod store;
30
31pub use accounting::GoalAccounting;
32pub use state::GoalStatus;
33pub use store::{GoalError, GoalStore};
34
35use chrono::{DateTime, Utc};
36
37/// A persisted long-horizon goal row.
38#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
39pub struct Goal {
40    /// Unique identifier (UUID v4 string).
41    pub id: String,
42    /// User-provided goal text (max `[goals] max_text_chars` chars at creation time).
43    pub text: String,
44    /// Current FSM status.
45    pub status: GoalStatus,
46    /// Optional token budget. `None` means unlimited.
47    pub token_budget: Option<i64>,
48    /// Number of conversation turns completed under this goal.
49    pub turns_used: i64,
50    /// Total tokens consumed across all turns under this goal.
51    pub tokens_used: i64,
52    /// When the goal was created.
53    pub created_at: DateTime<Utc>,
54    /// Last mutation time; used as a CAS guard for stale-update detection.
55    pub updated_at: DateTime<Utc>,
56    /// When the goal reached `Completed` or `Cleared`. `None` while active or paused.
57    pub completed_at: Option<DateTime<Utc>>,
58}
59
60/// Lightweight, cross-crate snapshot of an active goal.
61///
62/// Carries only what TUI and command handlers need, without pulling `zeph-core` into `zeph-tui`.
63#[derive(Debug, Clone, serde::Serialize)]
64pub struct GoalSnapshot {
65    /// UUID string of the goal.
66    pub id: String,
67    /// Goal text, pre-validated to fit within `max_text_chars`.
68    pub text: String,
69    /// Current FSM status.
70    pub status: GoalStatus,
71    /// Number of turns completed.
72    pub turns_used: u64,
73    /// Total tokens consumed.
74    pub tokens_used: u64,
75    /// Optional token budget (`None` = unlimited).
76    pub token_budget: Option<u64>,
77}
78
79impl From<Goal> for GoalSnapshot {
80    fn from(g: Goal) -> Self {
81        Self {
82            id: g.id,
83            text: g.text,
84            status: g.status,
85            turns_used: g.turns_used.max(0).cast_unsigned(),
86            tokens_used: g.tokens_used.max(0).cast_unsigned(),
87            token_budget: g.token_budget.map(|b| b.max(0).cast_unsigned()),
88        }
89    }
90}