Skip to main content

agent_core/
task.rs

1// Bounded cooperative tasks for long native/browser operations. New shared implementation.
2
3use crate::{ActionError, ActionResult, ErrorCode, Value};
4use alloc::{string::String, vec::Vec};
5
6/// State of a cooperative host operation.
7#[derive(Clone, Copy, Debug, PartialEq, Eq)]
8#[non_exhaustive]
9pub enum Status {
10    /// Queued but not started.
11    Pending,
12    /// Executing between interruption points.
13    Running,
14    /// Cancellation was requested; a host checkpoint still has to acknowledge it.
15    Cancelling,
16    /// Finished successfully.
17    Complete,
18    /// Finished with an action error.
19    Failed,
20    /// Cancellation was acknowledged at an interruption point.
21    Cancelled,
22}
23
24impl Status {
25    /// Stable status spelling.
26    pub fn as_str(self) -> &'static str {
27        match self {
28            Self::Pending => "pending",
29            Self::Running => "running",
30            Self::Cancelling => "cancelling",
31            Self::Complete => "complete",
32            Self::Failed => "failed",
33            Self::Cancelled => "cancelled",
34        }
35    }
36    /// Whether no more computation should be performed.
37    pub fn terminal(self) -> bool {
38        matches!(self, Self::Complete | Self::Failed | Self::Cancelled)
39    }
40}
41
42/// One task, isolated inside its app/connection owner.
43#[derive(Clone, Debug)]
44pub struct Task {
45    /// Monotonically allocated identifier; never reused by a table.
46    pub id: u64,
47    /// Semantic action that created the task.
48    pub action: String,
49    /// Current lifecycle state.
50    pub status: Status,
51    /// Progress in thousandths, when available.
52    pub progress: Option<u16>,
53    /// Short host-provided progress description.
54    pub message: String,
55    /// Whether cancellation is supported at host interruption points.
56    pub cancellable: bool,
57    /// Completed result, available only in the Complete state.
58    pub result: Option<ActionResult>,
59    /// Terminal failure or cancellation diagnosis.
60    pub error: Option<ActionError>,
61}
62
63impl Task {
64    /// Compact poll response. Binary resources remain links inside action results.
65    pub fn to_json(&self) -> Value {
66        Value::object([
67            ("id", Value::u64(self.id)),
68            ("action", Value::string(&self.action)),
69            ("status", Value::string(self.status.as_str())),
70            (
71                "progress",
72                self.progress
73                    .map(|p| Value::u64(p as u64))
74                    .unwrap_or(Value::Null),
75            ),
76            ("message", Value::string(&self.message)),
77            ("cancellable", Value::Bool(self.cancellable)),
78            (
79                "result",
80                self.result
81                    .as_ref()
82                    .map(ActionResult::to_json)
83                    .unwrap_or(Value::Null),
84            ),
85            (
86                "error",
87                self.error
88                    .as_ref()
89                    .map(ActionError::to_json)
90                    .unwrap_or(Value::Null),
91            ),
92        ])
93    }
94}
95
96/// Bounded in-memory task owner; a host drives work by calling checkpoint/update/finish.
97pub struct TaskTable {
98    tasks: Vec<Task>,
99    next_id: u64,
100    capacity: usize,
101}
102
103impl Default for TaskTable {
104    fn default() -> Self {
105        Self::new(128)
106    }
107}
108
109impl TaskTable {
110    /// Create a table; capacity is clamped to 1..=1024 tasks.
111    pub fn new(capacity: usize) -> Self {
112        Self {
113            tasks: Vec::new(),
114            next_id: 1,
115            capacity: capacity.clamp(1, 1024),
116        }
117    }
118    /// Allocate a pending task. Completed tasks are retained until explicitly forgotten or,
119    /// when the table is full, evicted oldest first.
120    pub fn create(&mut self, action: &str, cancellable: bool) -> Result<u64, ActionError> {
121        if self.tasks.len() >= self.capacity {
122            if let Some(oldest) = self.tasks.iter().position(|t| t.status.terminal()) {
123                self.tasks.remove(oldest);
124            }
125        }
126        if self.tasks.len() >= self.capacity || self.next_id == u64::MAX {
127            return Err(ActionError::new(
128                ErrorCode::LimitExceeded,
129                "Task table is full",
130            ));
131        }
132        if action.is_empty() || action.len() > 128 {
133            return Err(ActionError::new(
134                ErrorCode::InvalidInput,
135                "Invalid task action name",
136            ));
137        }
138        let id = self.next_id;
139        self.next_id += 1;
140        self.tasks.push(Task {
141            id,
142            action: action.into(),
143            status: Status::Pending,
144            progress: None,
145            message: String::new(),
146            cancellable,
147            result: None,
148            error: None,
149        });
150        Ok(id)
151    }
152    /// Inspect one task, without allowing external callers to mutate its state directly.
153    pub fn get(&self, id: u64) -> Result<&Task, ActionError> {
154        self.tasks.iter().find(|t| t.id == id).ok_or_else(not_found)
155    }
156    fn get_mut(&mut self, id: u64) -> Result<&mut Task, ActionError> {
157        self.tasks
158            .iter_mut()
159            .find(|t| t.id == id)
160            .ok_or_else(not_found)
161    }
162    /// Request cancellation; completion is reported only when the host acknowledges it.
163    pub fn request_cancel(&mut self, id: u64) -> Result<&Task, ActionError> {
164        let task = self.get_mut(id)?;
165        if task.status.terminal() {
166            return Ok(task);
167        }
168        if !task.cancellable {
169            return Err(ActionError::new(
170                ErrorCode::Unsupported,
171                "This task does not support cancellation",
172            ));
173        }
174        task.status = Status::Cancelling;
175        Ok(task)
176    }
177    /// Enter a safe interruption point. False means cancelled/finished; do no more work.
178    pub fn checkpoint(&mut self, id: u64) -> Result<bool, ActionError> {
179        let task = self.get_mut(id)?;
180        if task.status == Status::Cancelling {
181            task.status = Status::Cancelled;
182            task.error = Some(ActionError::new(
183                ErrorCode::Cancelled,
184                "Task cancelled at an interruption point",
185            ));
186            return Ok(false);
187        }
188        if task.status.terminal() {
189            return Ok(false);
190        }
191        task.status = Status::Running;
192        Ok(true)
193    }
194    /// Update progress; host work must still call checkpoint before each interruptible chunk.
195    pub fn update(
196        &mut self,
197        id: u64,
198        progress: Option<u16>,
199        message: &str,
200    ) -> Result<(), ActionError> {
201        if progress.is_some_and(|p| p > 1000) || message.len() > 4096 {
202            return Err(ActionError::new(
203                ErrorCode::InvalidInput,
204                "Invalid task progress",
205            ));
206        }
207        let task = self.get_mut(id)?;
208        if task.status.terminal() {
209            return Err(ActionError::new(
210                ErrorCode::Unavailable,
211                "Task already finished",
212            ));
213        }
214        task.progress = progress;
215        task.message = message.into();
216        Ok(())
217    }
218    /// Finish a task. A pending cancellation must first be resolved at a checkpoint.
219    pub fn finish(&mut self, id: u64, result: ActionResult) -> Result<(), ActionError> {
220        let task = self.get_mut(id)?;
221        if task.status.terminal() || task.status == Status::Cancelling {
222            return Err(ActionError::new(
223                ErrorCode::Unavailable,
224                "Task is finished or awaiting cancellation",
225            ));
226        }
227        if result.task.is_some() {
228            return Err(ActionError::new(
229                ErrorCode::InvalidInput,
230                "Task completion cannot defer itself",
231            ));
232        }
233        task.status = Status::Complete;
234        task.progress = Some(1000);
235        task.result = Some(result);
236        Ok(())
237    }
238    /// Fail a pending/running task with structured diagnostic details.
239    pub fn fail(&mut self, id: u64, error: ActionError) -> Result<(), ActionError> {
240        let task = self.get_mut(id)?;
241        if task.status.terminal() {
242            return Err(ActionError::new(
243                ErrorCode::Unavailable,
244                "Task already finished",
245            ));
246        }
247        task.status = Status::Failed;
248        task.error = Some(error);
249        Ok(())
250    }
251    /// Remove only a terminal task, retaining monotonic IDs to prevent accidental reuse.
252    pub fn forget(&mut self, id: u64) -> Result<(), ActionError> {
253        let Some(index) = self.tasks.iter().position(|t| t.id == id) else {
254            return Err(not_found());
255        };
256        if !self.tasks[index].status.terminal() {
257            return Err(ActionError::new(
258                ErrorCode::Unavailable,
259                "Running tasks cannot be forgotten",
260            ));
261        }
262        self.tasks.remove(index);
263        Ok(())
264    }
265    /// One ID-ordered page of task metadata.
266    pub fn list(&self, after: u64, limit: usize) -> Result<Value, ActionError> {
267        if limit == 0 || limit > 100 {
268            return Err(ActionError::new(
269                ErrorCode::InvalidInput,
270                "Task page limit is 1 to 100",
271            ));
272        }
273        let tasks: Vec<&Task> = self
274            .tasks
275            .iter()
276            .filter(|t| t.id > after)
277            .take(limit + 1)
278            .collect();
279        let cursor = if tasks.len() > limit {
280            Value::u64(tasks[limit - 1].id)
281        } else {
282            Value::Null
283        };
284        Ok(Value::object([
285            (
286                "tasks",
287                Value::Array(tasks.into_iter().take(limit).map(Task::to_json).collect()),
288            ),
289            ("nextCursor", cursor),
290        ]))
291    }
292}
293
294fn not_found() -> ActionError {
295    ActionError::new(ErrorCode::NotFound, "Task not found in this instance")
296}