supercode_harness/goals.rs
1//! BP-7 (catalog §4a "Goals (persistent objective across turns)", cc's
2//! `/goal`, cx's `/goal` + `goals_1.sqlite`): the session's standing
3//! objective.
4//!
5//! **Not the plan.** `update_plan` (the `todos` module's tool) holds a
6//! steps/status array for the CURRENT stretch of work and is deliberately
7//! ephemeral. A goal is the condition the whole session is working toward:
8//! one sentence, set once, restated to the model on every request until it
9//! is changed or cleared, and persisted beside the session so it survives a
10//! resume. Design §2 module 7 homes goals with `todos` for exactly this
11//! reason — same module, different lifetime.
12//!
13//! **Where it lands.** `<session>.goal.json`, a single typed record in the
14//! sidecar family next to `<session>.git.json` (also a single record, not a
15//! log) — never inside the provider-visible transcript, so translating a
16//! session to another harness never has to invent a message for it.
17
18use serde::{Deserialize, Serialize};
19
20/// The session's persistent objective.
21#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
22pub struct GoalRecord {
23 /// The objective, as the user stated it.
24 pub objective: String,
25 /// Unix-ms wall-clock time the goal was first set.
26 pub created_at_ms: i64,
27 /// Unix-ms wall-clock time it was last changed.
28 pub updated_at_ms: i64,
29}
30
31impl GoalRecord {
32 /// A goal set now.
33 pub fn new(objective: impl Into<String>, now_ms: i64) -> GoalRecord {
34 GoalRecord {
35 objective: objective.into(),
36 created_at_ms: now_ms,
37 updated_at_ms: now_ms,
38 }
39 }
40
41 /// Replace the objective, keeping `created_at_ms` — the goal's identity
42 /// is the session's, not the sentence's, so a reworded goal is the same
43 /// goal refined, not a new one.
44 pub fn revise(&mut self, objective: impl Into<String>, now_ms: i64) {
45 self.objective = objective.into();
46 self.updated_at_ms = now_ms;
47 }
48
49 /// The block spliced into the tail of every request while this goal
50 /// stands. Deliberately at the TAIL, not the system prompt: a standing
51 /// objective is only useful if it is the last thing the model reads
52 /// before the current turn, and appending never disturbs the cached
53 /// prefix.
54 pub fn reminder(&self) -> String {
55 format!(
56 "<goal>\nThe standing objective for this session is:\n{}\n\
57 Keep working toward it; say so plainly if it is already met or \
58 if it cannot be.\n</goal>",
59 self.objective.trim()
60 )
61 }
62}
63
64#[cfg(test)]
65mod tests {
66 use super::*;
67
68 #[test]
69 fn a_revision_keeps_the_creation_stamp_and_moves_the_update_stamp() {
70 let mut goal = GoalRecord::new("ship BP-7", 1_000);
71 goal.revise("ship BP-7 with proof", 2_000);
72 assert_eq!(goal.objective, "ship BP-7 with proof");
73 assert_eq!(goal.created_at_ms, 1_000);
74 assert_eq!(goal.updated_at_ms, 2_000);
75 }
76
77 #[test]
78 fn the_reminder_carries_the_objective_in_a_tagged_block() {
79 let goal = GoalRecord::new(" ship BP-7 ", 0);
80 let reminder = goal.reminder();
81 assert!(reminder.starts_with("<goal>"));
82 assert!(reminder.ends_with("</goal>"));
83 assert!(reminder.contains("ship BP-7"));
84 assert!(
85 !reminder.contains(" ship BP-7 "),
86 "the objective is trimmed"
87 );
88 }
89
90 #[test]
91 fn json_round_trip_is_lossless() {
92 let goal = GoalRecord::new("ship BP-7", 1_700_000_000_000);
93 let json = serde_json::to_string(&goal).unwrap();
94 assert_eq!(serde_json::from_str::<GoalRecord>(&json).unwrap(), goal);
95 }
96}