use std::sync::Arc;
use anyhow::Result;
use tokio::sync::Mutex;
use crate::memory::{MemoryDoc, MemoryStore};
pub const GOAL_KIND: &str = "active_goal";
pub const DEFAULT_MAX_ITER: u32 = 30;
pub const HARD_MAX_ITER: u32 = 200;
#[derive(Debug, Clone)]
pub struct ActiveGoal {
pub condition: String,
pub iter: u32,
pub max_iter: u32,
pub started_at: i64,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TerminalSignal {
Achieved,
Failed(String),
Continue,
}
pub enum Reaction {
Done(String),
Continue(String),
}
pub fn parse_terminal(reply_text: &str) -> TerminalSignal {
let last = reply_text
.lines()
.rev()
.find(|l| !l.trim().is_empty())
.unwrap_or("")
.trim();
if last == "GOAL_ACHIEVED" {
return TerminalSignal::Achieved;
}
if let Some(rest) = last.strip_prefix("GOAL_FAILED") {
return TerminalSignal::Failed(rest.trim().to_owned());
}
TerminalSignal::Continue
}
pub async fn read(
mem: &Arc<Mutex<MemoryStore>>,
session_key: &str,
) -> Option<ActiveGoal> {
let store = mem.lock().await;
let docs = store.list_active();
drop(store);
docs.into_iter()
.find(|d| d.scope == session_key && d.kind == GOAL_KIND)
.map(|d| {
let (iter, max_iter) = parse_iter_meta(d.abstract_text.as_deref().unwrap_or(""));
ActiveGoal {
condition: d.text,
iter,
max_iter,
started_at: d.created_at,
}
})
}
pub async fn set(
mem: &Arc<Mutex<MemoryStore>>,
session_key: &str,
condition: &str,
max_iter: u32,
) -> Result<()> {
let max_iter = max_iter.clamp(1, HARD_MAX_ITER);
clear(mem, session_key).await?;
let now = chrono::Utc::now().timestamp();
let mut store = mem.lock().await;
let doc = MemoryDoc {
id: uuid::Uuid::new_v4().to_string(),
scope: session_key.to_owned(),
kind: GOAL_KIND.to_owned(),
text: condition.to_owned(),
vector: vec![],
created_at: now,
accessed_at: 0,
access_count: 0,
importance: 0.9,
tier: Default::default(),
abstract_text: Some(format!(r#"{{"iter":1,"max":{max_iter}}}"#)),
overview_text: None,
tags: vec!["goal".to_owned()],
pinned: true,
};
store.add(doc).await?;
Ok(())
}
pub async fn clear(
mem: &Arc<Mutex<MemoryStore>>,
session_key: &str,
) -> Result<()> {
let store = mem.lock().await;
let to_delete: Vec<String> = store
.list_active()
.into_iter()
.filter(|d| d.scope == session_key && d.kind == GOAL_KIND)
.map(|d| d.id)
.collect();
drop(store);
if to_delete.is_empty() {
return Ok(());
}
let mut store = mem.lock().await;
for id in to_delete {
let _ = store.delete(&id).await;
}
Ok(())
}
async fn bump_iter(
mem: &Arc<Mutex<MemoryStore>>,
session_key: &str,
current: &ActiveGoal,
) -> Result<()> {
let store = mem.lock().await;
let existing_id: Option<String> = store
.list_active()
.into_iter()
.find(|d| d.scope == session_key && d.kind == GOAL_KIND)
.map(|d| d.id);
drop(store);
let Some(old_id) = existing_id else {
return Ok(());
};
let mut store = mem.lock().await;
let _ = store.delete(&old_id).await;
let doc = MemoryDoc {
id: uuid::Uuid::new_v4().to_string(),
scope: session_key.to_owned(),
kind: GOAL_KIND.to_owned(),
text: current.condition.clone(),
vector: vec![],
created_at: current.started_at,
accessed_at: 0,
access_count: 0,
importance: 0.9,
tier: Default::default(),
abstract_text: Some(format!(
r#"{{"iter":{},"max":{}}}"#,
current.iter + 1,
current.max_iter
)),
overview_text: None,
tags: vec!["goal".to_owned()],
pinned: true,
};
store.add(doc).await?;
Ok(())
}
pub async fn check_after_turn(session_key: &str, reply_text: &str) -> Option<Reaction> {
let mem = crate::memory::global_store()?;
let active = read(&mem, session_key).await?;
let signal = parse_terminal(reply_text);
match signal {
TerminalSignal::Achieved => {
let _ = clear(&mem, session_key).await;
Some(Reaction::Done(format!(
"✅ Goal achieved (iter {}/{}): {}",
active.iter, active.max_iter, active.condition
)))
}
TerminalSignal::Failed(reason) => {
let _ = clear(&mem, session_key).await;
let msg = if reason.is_empty() {
format!(
"❌ Goal could not be achieved (iter {}/{}): {}",
active.iter, active.max_iter, active.condition
)
} else {
format!(
"❌ Goal could not be achieved (iter {}/{}): {}\n原因: {}",
active.iter, active.max_iter, active.condition, reason
)
};
Some(Reaction::Done(msg))
}
TerminalSignal::Continue => {
if active.iter >= active.max_iter {
let _ = clear(&mem, session_key).await;
Some(Reaction::Done(format!(
"⚠ Goal hit iter cap ({}): {} — auto-stopped. Type `/goal {}` to restart.",
active.max_iter, active.condition, active.condition
)))
} else {
let _ = bump_iter(&mem, session_key, &active).await;
Some(Reaction::Continue(build_continuation_prompt(&active)))
}
}
}
}
fn build_continuation_prompt(g: &ActiveGoal) -> String {
format!(
"目标: {} (iter {}/{})\n\n\
你处于 /goal 模式。继续推进。\n\
回复末行严格写其中一种 (区分大小写):\n\
\u{0020}\u{0020}GOAL_ACHIEVED → 已达成,我会停\n\
\u{0020}\u{0020}GOAL_FAILED <理由> → 放弃,我会停\n\
\u{0020}\u{0020}(不写 GOAL_* 一行) → 继续下一轮\n\n\
如果上一轮已经达成,直接以 GOAL_ACHIEVED 收尾即可。",
g.condition,
g.iter + 1,
g.max_iter
)
}
pub fn build_initial_prompt(condition: &str, max_iter: u32) -> String {
format!(
"目标: {} (iter 1/{})\n\n\
你处于 /goal 模式。开始干活推进这个目标。\n\
回复末行严格写其中一种 (区分大小写):\n\
\u{0020}\u{0020}GOAL_ACHIEVED → 已达成,我会停\n\
\u{0020}\u{0020}GOAL_FAILED <理由> → 放弃,我会停\n\
\u{0020}\u{0020}(不写 GOAL_* 一行) → 继续下一轮\n\n\
如果目标看起来已经达成 (例如静态条件已满足),直接 GOAL_ACHIEVED 即可。",
condition, max_iter
)
}
fn parse_iter_meta(raw: &str) -> (u32, u32) {
let v: serde_json::Value = serde_json::from_str(raw).unwrap_or(serde_json::Value::Null);
let iter = v.get("iter").and_then(|x| x.as_u64()).unwrap_or(1) as u32;
let max = v
.get("max")
.and_then(|x| x.as_u64())
.unwrap_or(DEFAULT_MAX_ITER as u64) as u32;
(iter.max(1), max.clamp(1, HARD_MAX_ITER))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_terminal_recognises_achieved() {
assert_eq!(
parse_terminal("doing stuff\n\nGOAL_ACHIEVED"),
TerminalSignal::Achieved
);
assert_eq!(
parse_terminal("doing stuff\n\nGOAL_ACHIEVED \n\n"),
TerminalSignal::Achieved
);
}
#[test]
fn parse_terminal_recognises_failed_with_reason() {
assert_eq!(
parse_terminal("explanation\n\nGOAL_FAILED no test runner"),
TerminalSignal::Failed("no test runner".to_owned())
);
assert_eq!(
parse_terminal("GOAL_FAILED"),
TerminalSignal::Failed(String::new())
);
}
#[test]
fn parse_terminal_continue_when_no_marker() {
assert_eq!(
parse_terminal("just an analysis paragraph"),
TerminalSignal::Continue
);
assert_eq!(
parse_terminal("GOAL_ACHIEVED\nactually wait i'm not sure"),
TerminalSignal::Continue
);
}
#[test]
fn parse_iter_meta_round_trip() {
assert_eq!(parse_iter_meta(r#"{"iter":3,"max":50}"#), (3, 50));
assert_eq!(parse_iter_meta(""), (1, DEFAULT_MAX_ITER));
assert_eq!(parse_iter_meta("garbage"), (1, DEFAULT_MAX_ITER));
assert_eq!(parse_iter_meta(r#"{"iter":1,"max":99999}"#).1, HARD_MAX_ITER);
}
}