Skip to main content

codeswarm_adapters/
goal.rs

1//! User-owned session goals, independent of provider sessions and relay limits.
2use serde::{Deserialize, Serialize};
3
4pub const GOAL_USAGE: &str = "/goal [OBJECTIVE | run | done | clear]";
5
6#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
7#[serde(rename_all = "snake_case")]
8pub enum GoalStatus {
9    Active,
10    Completed,
11}
12
13#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
14pub struct Goal {
15    pub objective: String,
16    pub status: GoalStatus,
17}
18
19#[derive(Clone, Debug, Eq, PartialEq)]
20pub enum GoalCommand {
21    Show,
22    Set(String),
23    Run,
24    Done,
25    Clear,
26}
27
28impl GoalCommand {
29    pub fn parse(argument: &str) -> Result<Self, String> {
30        let text = argument.trim();
31        let mut parts = text.split_whitespace();
32        let Some(first) = parts.next() else {
33            return Ok(Self::Show);
34        };
35        let reserved = match first.to_ascii_lowercase().as_str() {
36            "run" => Some(Self::Run),
37            "done" => Some(Self::Done),
38            "clear" => Some(Self::Clear),
39            _ => None,
40        };
41        if let Some(command) = reserved {
42            return if parts.next().is_none() {
43                Ok(command)
44            } else {
45                Err(format!("usage: {GOAL_USAGE}"))
46            };
47        }
48        validate_objective(text)?;
49        Ok(Self::Set(text.into()))
50    }
51}
52
53fn validate_objective(text: &str) -> Result<(), String> {
54    if text.trim().is_empty() {
55        return Err("goal objective must not be empty".into());
56    }
57    if text.len() > 16_000 {
58        return Err("goal objective must be at most 16,000 bytes".into());
59    }
60    Ok(())
61}
62
63impl Goal {
64    pub fn from_metadata(value: &serde_json::Value) -> Option<Self> {
65        let goal: Self = serde_json::from_value(value.clone()).ok()?;
66        validate_objective(&goal.objective).ok()?;
67        Some(goal)
68    }
69    pub fn summary(&self) -> String {
70        format!(
71            "Goal {}: {}",
72            match self.status {
73                GoalStatus::Active => "active",
74                GoalStatus::Completed => "completed",
75            },
76            self.objective
77        )
78    }
79}
80
81/// Returns a human task to dispatch only for setting or resuming a goal.
82pub fn apply(goal: &mut Option<Goal>, command: GoalCommand) -> Result<Option<String>, String> {
83    match command {
84        GoalCommand::Set(objective) => {
85            validate_objective(&objective)?;
86            let task = format!("Work toward this goal: {}", objective.trim());
87            *goal = Some(Goal {
88                objective: objective.trim().into(),
89                status: GoalStatus::Active,
90            });
91            Ok(Some(task))
92        }
93        GoalCommand::Run => match goal {
94            Some(goal) if goal.status == GoalStatus::Active => Ok(Some(format!(
95                "Continue working toward this goal: {}",
96                goal.objective
97            ))),
98            _ => Err("no active goal; use /goal OBJECTIVE to start one".into()),
99        },
100        GoalCommand::Done => {
101            let goal = goal.as_mut().ok_or("no goal to complete")?;
102            goal.status = GoalStatus::Completed;
103            Ok(None)
104        }
105        GoalCommand::Clear => {
106            *goal = None;
107            Ok(None)
108        }
109        GoalCommand::Show => Ok(None),
110    }
111}
112
113pub fn prompt(goal: Option<&Goal>, task: &str) -> String {
114    let context = match goal {
115        Some(goal) if goal.status == GoalStatus::Active => format!(
116            "Active shared goal: {}\nWork toward this objective. The current user request takes priority. Report progress and remaining work honestly; completion is tracked by the user with /goal done. Respect permissions and relay limits.",
117            goal.objective
118        ),
119        _ => "No active shared goal. Follow the current user request.".into(),
120    };
121    format!("{task}\n\n[CodeSwarm goal context]\n{context}")
122}
123
124#[cfg(test)]
125mod tests {
126    use super::*;
127    #[test]
128    fn goal_lifecycle_and_invalid_metadata_are_explicit() {
129        let mut goal = None;
130        assert!(apply(&mut goal, GoalCommand::Run).is_err());
131        assert!(apply(&mut goal, GoalCommand::Done).is_err());
132        assert_eq!(GoalCommand::parse("").unwrap(), GoalCommand::Show);
133        assert!(GoalCommand::parse("run extra").is_err());
134        assert!(GoalCommand::parse(&"x".repeat(16_001)).is_err());
135        let objective = "Fix login\n  preserve existing sessions";
136        let action = GoalCommand::parse(objective).unwrap();
137        assert!(
138            apply(&mut goal, action)
139                .unwrap()
140                .unwrap()
141                .contains(objective)
142        );
143        let encoded = serde_json::to_value(&goal).unwrap();
144        assert_eq!(Goal::from_metadata(&encoded), goal);
145        assert!(prompt(goal.as_ref(), "review").contains(objective));
146        apply(&mut goal, GoalCommand::Done).unwrap();
147        assert!(apply(&mut goal, GoalCommand::Run).is_err());
148        assert!(!prompt(goal.as_ref(), "new request").contains(objective));
149        apply(&mut goal, GoalCommand::Clear).unwrap();
150        assert!(goal.is_none());
151        for value in [
152            serde_json::json!(null),
153            serde_json::json!({"objective":"", "status":"active"}),
154            serde_json::json!({"objective":"task", "status":"unknown"}),
155            serde_json::json!({"objective":42,"status":"active"}),
156        ] {
157            assert!(Goal::from_metadata(&value).is_none());
158        }
159    }
160}