rx4 0.7.2

The agent harness engine — loop, tools, providers, sessions, permissions, computer-use
Documentation
use crate::avo::{objective_f, LineageScore};
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Evidence {
    pub claim_id: String,
    pub actor_id: String,
    pub note: String,
}

#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct EvidenceLedger {
    pub entries: Vec<Evidence>,
}

impl EvidenceLedger {
    pub fn record(&mut self, evidence: Evidence) {
        self.entries.push(evidence);
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SubtaskStatus {
    Open,
    Claimed,
    Complete,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Subtask {
    pub id: String,
    pub parent_id: Option<String>,
    pub status: SubtaskStatus,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SubtaskClaim {
    pub actor_id: String,
    pub target_id: String,
    pub evidence: Evidence,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum HostAdjudication {
    Accept,
    Reject,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ClaimOutcome {
    Recorded,
    RejectedChildMarkedParent,
    RejectedHost,
    RejectedUnknown,
}

fn is_descendant(tasks: &[Subtask], ancestor: &str, node: &str) -> bool {
    let mut current = node;
    let mut guard = 0usize;
    while guard < tasks.len() {
        let Some(task) = tasks.iter().find(|t| t.id == current) else {
            return false;
        };
        match task.parent_id.as_deref() {
            Some(parent) if parent == ancestor => return true,
            Some(parent) => current = parent,
            None => return false,
        }
        guard += 1;
    }
    false
}

pub fn claim_complete(
    tasks: &mut [Subtask],
    ledger: &mut EvidenceLedger,
    claim: SubtaskClaim,
    host: HostAdjudication,
) -> ClaimOutcome {
    if host != HostAdjudication::Accept {
        return ClaimOutcome::RejectedHost;
    }
    if !tasks.iter().any(|t| t.id == claim.target_id) {
        return ClaimOutcome::RejectedUnknown;
    }
    let downward = claim.actor_id == claim.target_id
        || is_descendant(tasks, &claim.actor_id, &claim.target_id);
    if !downward {
        return ClaimOutcome::RejectedChildMarkedParent;
    }
    if let Some(task) = tasks.iter_mut().find(|t| t.id == claim.target_id) {
        task.status = SubtaskStatus::Complete;
    }
    ledger.record(claim.evidence);
    ClaimOutcome::Recorded
}

pub fn avo_score_for_claim(incorrect: bool, quality: f64) -> LineageScore {
    LineageScore {
        id: "subtask".into(),
        p_t: 1.0,
        incorrect,
        quality: objective_f(incorrect, quality),
    }
}

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

    fn tree() -> Vec<Subtask> {
        vec![
            Subtask {
                id: "parent".into(),
                parent_id: None,
                status: SubtaskStatus::Open,
            },
            Subtask {
                id: "child".into(),
                parent_id: Some("parent".into()),
                status: SubtaskStatus::Open,
            },
        ]
    }

    fn claim(actor: &str, target: &str) -> SubtaskClaim {
        SubtaskClaim {
            actor_id: actor.into(),
            target_id: target.into(),
            evidence: Evidence {
                claim_id: format!("{actor}->{target}"),
                actor_id: actor.into(),
                note: "done".into(),
            },
        }
    }

    #[test]
    fn child_cannot_mark_parent_complete() {
        let mut tasks = tree();
        let mut ledger = EvidenceLedger::default();
        let outcome = claim_complete(
            &mut tasks,
            &mut ledger,
            claim("child", "parent"),
            HostAdjudication::Accept,
        );
        assert_eq!(outcome, ClaimOutcome::RejectedChildMarkedParent);
        assert_eq!(tasks[0].status, SubtaskStatus::Open);
        assert!(ledger.entries.is_empty());
    }

    #[test]
    fn host_rejects_without_recording() {
        let mut tasks = tree();
        let mut ledger = EvidenceLedger::default();
        let outcome = claim_complete(
            &mut tasks,
            &mut ledger,
            claim("parent", "child"),
            HostAdjudication::Reject,
        );
        assert_eq!(outcome, ClaimOutcome::RejectedHost);
        assert!(ledger.entries.is_empty());
    }

    #[test]
    fn parent_can_mark_child_and_self() {
        let mut tasks = tree();
        let mut ledger = EvidenceLedger::default();
        assert_eq!(
            claim_complete(
                &mut tasks,
                &mut ledger,
                claim("parent", "child"),
                HostAdjudication::Accept
            ),
            ClaimOutcome::Recorded
        );
        assert_eq!(tasks[1].status, SubtaskStatus::Complete);
        assert_eq!(ledger.entries.len(), 1);
        assert_eq!(
            claim_complete(
                &mut tasks,
                &mut ledger,
                claim("child", "child"),
                HostAdjudication::Accept
            ),
            ClaimOutcome::Recorded
        );
    }
}