use std::sync::Arc;
use std::time::Duration;
use super::store;
use super::types::{GoalProgress, ThreadGoal, ThreadGoalStatus, TurnOutcome};
use crate::error::Result;
use crate::graph::builder::{END, NodeContext, NodeFuture};
use crate::graph::command::{Command, NodeResult};
use crate::harness::store::Store;
pub fn goal_gate_node<State, Update>(
store: Arc<dyn Store>,
work_node: impl Into<crate::harness::ids::NodeId>,
progress: impl Fn(&State) -> GoalProgress + Send + Sync + 'static,
) -> impl Fn(State, NodeContext) -> NodeFuture<Update> + Send + Sync + 'static
where
State: Send + 'static,
Update: Send + 'static,
{
let work_node = work_node.into();
let progress = Arc::new(progress);
move |state, ctx| {
let store = store.clone();
let work_node = work_node.clone();
let progress = progress.clone();
Box::pin(async move {
let Some(thread_id) = ctx.thread_id.as_ref().map(|t| t.as_str().to_string()) else {
return Ok(NodeResult::Command(Command::goto([END])));
};
let Some(goal) = store::get(&store, &thread_id).await? else {
return Ok(NodeResult::Command(Command::goto([END])));
};
let p = progress(&state);
let goal = match store::account_usage(
&store,
&thread_id,
&goal.goal_id,
p.tokens_used,
p.elapsed_secs,
)
.await?
{
Some(g) => g,
None => return Ok(NodeResult::Command(Command::goto([END]))),
};
if !goal.status.is_active() || goal.continuation_suppressed {
return Ok(NodeResult::Command(Command::goto([END])));
}
if !p.made_progress {
store::set_continuation_suppressed_if(&store, &thread_id, &goal.goal_id, true)
.await?;
return Ok(NodeResult::Command(Command::goto([END])));
}
Ok(NodeResult::Command(Command::goto([work_node])))
})
}
}
pub async fn run_continuation_tick<F, Fut>(
store: &Arc<dyn Store>,
idle: Duration,
max_per_tick: usize,
run_turn: F,
) -> Result<usize>
where
F: Fn(ThreadGoal) -> Fut,
Fut: Future<Output = Result<TurnOutcome>>,
{
let now = crate::harness::ids::now_ms();
let idle_ms = idle.as_millis() as u64;
let mut candidates: Vec<ThreadGoal> = store::list_all(store)
.await?
.into_iter()
.filter(|g| g.status.is_active())
.filter(|g| !g.continuation_suppressed)
.filter(|g| now.saturating_sub(g.updated_at_ms) >= idle_ms)
.collect();
candidates.sort_by_key(|g| g.updated_at_ms);
candidates.truncate(max_per_tick);
let mut ran = 0;
for goal in candidates {
let outcome = match run_turn(goal.clone()).await {
Ok(o) => o,
Err(_) => continue,
};
store::account_usage(
store,
&goal.thread_id,
&goal.goal_id,
outcome.tokens_used,
outcome.elapsed_secs,
)
.await?;
if !outcome.made_progress {
store::set_continuation_suppressed_if(store, &goal.thread_id, &goal.goal_id, true)
.await?;
}
ran += 1;
}
Ok(ran)
}
pub async fn note_user_turn(store: &Arc<dyn Store>, thread_id: &str) -> Result<Option<ThreadGoal>> {
let Some(goal) = store::get(store, thread_id).await? else {
return Ok(None);
};
match goal.status {
ThreadGoalStatus::Paused => store::resume(store, thread_id).await.map(Some),
ThreadGoalStatus::Active if goal.continuation_suppressed => {
store::set_continuation_suppressed_if(store, thread_id, &goal.goal_id, false).await
}
_ => Ok(Some(goal)),
}
}