rx4 0.7.2

The agent harness engine — loop, tools, providers, sessions, permissions, computer-use
Documentation
//! Session-scoped task tracking with confidence-gated completion.

use serde::{Deserialize, Serialize};

/// Lifecycle state of a todo item.
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum TodoStatus {
    #[default]
    Pending,
    InProgress,
    PendingVerification,
    Completed,
}

/// A task created by the model during an agent session.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct TodoItem {
    /// Stable item identifier supplied by the model or generated by the engine.
    pub id: String,
    /// Human-readable task description.
    pub content: String,
    /// Current lifecycle state.
    #[serde(default)]
    pub status: TodoStatus,
    /// Model confidence in the approach when the task was created (0–100).
    pub creation_confidence: u8,
    /// Model confidence supplied for the most recent completion attempt.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub completion_confidence: Option<u8>,
    /// Number of completion attempts that required evidence verification.
    #[serde(default)]
    pub verification_attempts: u8,
}

/// Serializable todo state held by a session.
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct TodoState {
    /// Items in display order.
    #[serde(default)]
    pub items: Vec<TodoItem>,
}

/// Confidence-gating behaviour for the opt-in todo engine.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct TodoConfig {
    /// Reject a first completion attempt when its confidence rises by more
    /// than this many points from creation confidence.
    pub completion_confidence_delta: u8,
}

impl Default for TodoConfig {
    fn default() -> Self {
        Self {
            completion_confidence_delta: 40,
        }
    }
}

/// Result of applying a todo tool request.
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct TodoMutation {
    pub state: TodoState,
    pub verification_required: bool,
}

pub(crate) fn apply_request(
    state: &mut TodoState,
    config: &TodoConfig,
    value: &serde_json::Value,
) -> Result<TodoMutation, String> {
    let action = value
        .get("action")
        .and_then(serde_json::Value::as_str)
        .ok_or_else(|| "action required".to_string())?;
    match action {
        "list" => {}
        "clear" => state.items.clear(),
        "add" | "create" => {
            let items = value
                .get("items")
                .and_then(serde_json::Value::as_array)
                .ok_or_else(|| "items required for create".to_string())?;
            if items.is_empty() {
                return Err("items required for create".into());
            }
            for item in items {
                let content = required_string(item, "content")?;
                let confidence = required_confidence(item, "confidence")?;
                let id = item
                    .get("id")
                    .and_then(serde_json::Value::as_str)
                    .filter(|id| !id.is_empty())
                    .map(str::to_owned)
                    .unwrap_or_else(|| uuid::Uuid::new_v4().to_string());
                if state.items.iter().any(|existing| existing.id == id) {
                    return Err(format!("todo id already exists: {id}"));
                }
                state.items.push(TodoItem {
                    id,
                    content,
                    status: TodoStatus::Pending,
                    creation_confidence: confidence,
                    completion_confidence: None,
                    verification_attempts: 0,
                });
            }
        }
        "update" => {
            let items = required_items(value, action)?;
            for item in items {
                let todo = find_item_mut(&mut state.items, item)?;
                if let Some(content) = item.get("content").and_then(serde_json::Value::as_str) {
                    todo.content = content.to_owned();
                }
                if let Some(status) = item.get("status").and_then(serde_json::Value::as_str) {
                    todo.status = parse_status(status)?;
                }
            }
        }
        "complete" => {
            let items = required_items(value, action)?;
            let mut verification_required = false;
            for item in items {
                let confidence = required_confidence(item, "completion_confidence")?;
                let todo = find_item_mut(&mut state.items, item)?;
                todo.completion_confidence = Some(confidence);
                let delta = confidence.saturating_sub(todo.creation_confidence);
                if todo.verification_attempts == 0 && delta > config.completion_confidence_delta {
                    todo.verification_attempts = 1;
                    todo.status = TodoStatus::PendingVerification;
                    verification_required = true;
                } else {
                    todo.status = TodoStatus::Completed;
                }
            }
            return Ok(TodoMutation {
                state: state.clone(),
                verification_required,
            });
        }
        other => return Err(format!("unknown action: {other}")),
    }
    Ok(TodoMutation {
        state: state.clone(),
        verification_required: false,
    })
}

fn required_items<'a>(
    value: &'a serde_json::Value,
    action: &str,
) -> Result<&'a [serde_json::Value], String> {
    value
        .get("items")
        .and_then(serde_json::Value::as_array)
        .filter(|items| !items.is_empty())
        .map(Vec::as_slice)
        .ok_or_else(|| format!("items required for {action}"))
}

fn find_item_mut<'a>(
    items: &'a mut [TodoItem],
    value: &serde_json::Value,
) -> Result<&'a mut TodoItem, String> {
    let id = required_string(value, "id")?;
    items
        .iter_mut()
        .find(|item| item.id == id)
        .ok_or_else(|| format!("todo not found: {id}"))
}

fn required_string(value: &serde_json::Value, field: &str) -> Result<String, String> {
    value
        .get(field)
        .and_then(serde_json::Value::as_str)
        .filter(|value| !value.trim().is_empty())
        .map(str::to_owned)
        .ok_or_else(|| format!("{field} required"))
}

fn required_confidence(value: &serde_json::Value, field: &str) -> Result<u8, String> {
    value
        .get(field)
        .and_then(serde_json::Value::as_u64)
        .filter(|value| *value <= 100)
        .map(|value| value as u8)
        .ok_or_else(|| format!("{field} must be an integer from 0 to 100"))
}

fn parse_status(status: &str) -> Result<TodoStatus, String> {
    match status {
        "pending" => Ok(TodoStatus::Pending),
        "in_progress" => Ok(TodoStatus::InProgress),
        "pending_verification" => Ok(TodoStatus::PendingVerification),
        "completed" => Ok(TodoStatus::Completed),
        _ => Err(format!("unknown todo status: {status}")),
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn high_confidence_jump_requires_one_verification() {
        let mut state = TodoState::default();
        apply_request(
            &mut state,
            &TodoConfig::default(),
            &serde_json::json!({"action":"create","items":[{"id":"one","content":"test","confidence":30}]}),
        )
        .unwrap();
        let first = apply_request(
            &mut state,
            &TodoConfig::default(),
            &serde_json::json!({"action":"complete","items":[{"id":"one","completion_confidence":80}]}),
        )
        .unwrap();
        assert!(first.verification_required);
        assert_eq!(state.items[0].status, TodoStatus::PendingVerification);
        let second = apply_request(
            &mut state,
            &TodoConfig::default(),
            &serde_json::json!({"action":"complete","items":[{"id":"one","completion_confidence":80}]}),
        )
        .unwrap();
        assert!(!second.verification_required);
        assert_eq!(state.items[0].status, TodoStatus::Completed);
    }
}