Skip to main content

mentra/agent/
task_state.rs

1use std::borrow::Cow;
2
3use crate::error::RuntimeError;
4use crate::runtime::{
5    TaskBoard, TaskStateSnapshot,
6    task::{TASK_REMINDER_TEXT, TaskAccess, TaskIntrinsicTool, has_unfinished_tasks},
7};
8
9use super::Agent;
10
11impl Agent {
12    /// Returns a task-board view with this agent's own access identity.
13    ///
14    /// Lead agents retain lead privileges. Teammates remain constrained to
15    /// their own tasks and cannot edit dependency edges. Task-board mutations
16    /// update the shared store immediately; this agent's cached snapshot is
17    /// refreshed at the next normal task refresh/run boundary.
18    pub fn task_board(&self) -> TaskBoard {
19        TaskBoard::agent(
20            self.runtime.clone(),
21            self.config.task.tasks_dir.clone(),
22            self.name.clone(),
23            self.teammate_identity.is_some(),
24        )
25    }
26
27    pub(crate) fn effective_system_prompt(&self) -> Option<Cow<'_, str>> {
28        let mut sections = Vec::new();
29
30        if self.rounds_since_task >= self.config.task.reminder_threshold
31            && has_unfinished_tasks(&self.tasks)
32        {
33            sections.push(TASK_REMINDER_TEXT.to_string());
34        }
35
36        if let Some(system) = &self.config.system {
37            sections.push(system.clone());
38        }
39
40        if let Some(skills) = self.runtime.skill_descriptions() {
41            sections.push(skills);
42        }
43
44        if sections.is_empty() {
45            None
46        } else {
47            Some(Cow::Owned(sections.join("\n\n")))
48        }
49    }
50
51    pub(crate) fn note_round_without_task(&mut self) {
52        if has_unfinished_tasks(&self.tasks) {
53            self.rounds_since_task += 1;
54        }
55    }
56
57    pub(crate) fn record_task_activity(&mut self) {
58        self.rounds_since_task = 0;
59    }
60
61    pub(crate) fn refresh_tasks_from_disk(&mut self) -> Result<(), RuntimeError> {
62        let tasks = self
63            .runtime
64            .store()
65            .load_tasks(self.config.task.tasks_dir.as_path())?;
66        self.tasks = tasks;
67        let tasks = self.tasks.clone();
68        self.mutate_snapshot(|snapshot| {
69            snapshot.tasks = tasks;
70        });
71        Ok(())
72    }
73
74    pub(crate) fn task_access(&self) -> TaskAccess<'_> {
75        match &self.teammate_identity {
76            Some(_) => TaskAccess::Teammate(self.name.as_str()),
77            None => TaskAccess::Lead,
78        }
79    }
80
81    pub(crate) fn try_claim_ready_task(
82        &mut self,
83    ) -> Result<Option<crate::runtime::TaskItem>, RuntimeError> {
84        self.refresh_tasks_from_disk()?;
85        if self.owns_unfinished_tasks() {
86            return Ok(None);
87        }
88
89        match self.execute_task_mutation(&TaskIntrinsicTool::Claim, serde_json::json!({})) {
90            Ok(content) => {
91                self.refresh_tasks_from_disk()?;
92                serde_json::from_str::<crate::runtime::TaskItem>(&content)
93                    .map(Some)
94                    .map_err(RuntimeError::FailedToSerializeTasks)
95            }
96            Err(error) if error == "No ready unowned tasks are available to claim" => Ok(None),
97            Err(error) => Err(RuntimeError::InvalidTask(error)),
98        }
99    }
100
101    pub(crate) fn execute_task_mutation(
102        &self,
103        tool: &TaskIntrinsicTool,
104        input: serde_json::Value,
105    ) -> Result<String, String> {
106        self.runtime.execute_task_mutation(
107            tool,
108            input,
109            self.config.task.tasks_dir.as_path(),
110            self.task_access(),
111        )
112    }
113
114    pub(super) fn capture_task_disk_state(&self) -> Result<TaskStateSnapshot, RuntimeError> {
115        self.runtime
116            .store()
117            .capture_tasks(self.config.task.tasks_dir.as_path())
118    }
119
120    fn owns_unfinished_tasks(&self) -> bool {
121        self.tasks.iter().any(|task| {
122            task.owner == self.name && !matches!(task.status, crate::runtime::TaskStatus::Completed)
123        })
124    }
125
126    pub(super) fn restore_task_state(
127        &mut self,
128        tasks: Vec<crate::runtime::TaskItem>,
129        rounds_since_task: usize,
130        disk_state: &TaskStateSnapshot,
131    ) -> Result<(), RuntimeError> {
132        self.runtime
133            .store()
134            .restore_tasks(self.config.task.tasks_dir.as_path(), disk_state)?;
135        self.tasks = tasks;
136        self.rounds_since_task = rounds_since_task;
137        let tasks = self.tasks.clone();
138        self.mutate_snapshot(|snapshot| {
139            snapshot.tasks = tasks;
140        });
141        Ok(())
142    }
143}