OXYGENHUB

Coverage / crates/spectra-core/src/rules.rs 99.0%

101 of 102 lines run, 21 of 37 functions · run #29 on main · run missed

1
//! Beacon (spec §12): the brief an agent receives, compiled from the
2
//! layers the owner has set (overall, then the agent's project), each under
3
//! a heading naming it, with no other change; the version is the BLAKE3 of
4
//! that text. Briefs are `brief.set` events signed by the owner; nothing is
5
//! read from a file.
6
7
use crate::event::Event;
8
use anyhow::{bail, Result};
9
10
/// A brief over this is refused (spec §12.1).
11
pub const MAX_BRIEF: usize = 64 * 1024;
12
13
/// A compiled brief and its version.
14
#[derive(Clone, Debug, PartialEq, Eq)]
15
pub struct Rules {
16
    pub text: String,
17
    /// The first 12 hex characters of BLAKE3(text).
18
    pub version: String,
19
}
20
21
/// Check a brief's text before it is signed.
2217×
pub fn check(text: &str) -> Result<()> {
2317×
    if text.len() > MAX_BRIEF {
242×
        bail!("a brief is at most 64 KiB, so an agent can read it in one call ({} bytes)", text.len());
2515×
    }
2615×
    Ok(())
2717×
}
28
29
/// The text of one layer, if set.
30267×
pub async fn layer(pool: &sqlx::SqlitePool, project: Option<&str>) -> Result<Option<String>> {
31267×
    let (layer, p) = match project {
32130×
        Some(p) => ("project", p),
33137×
        None => ("overall", ""),
34
    };
35267×
    Ok(sqlx::query_scalar("SELECT text FROM briefs WHERE layer = ? AND project = ?").bind(layer).bind(p).fetch_optional(pool).await?)
36267×
}
37
38
/// The brief for an agent in `project` (none for the owner: the overall
39
/// brief alone).
40132×
pub async fn compile(pool: &sqlx::SqlitePool, project: Option<&str>) -> Result<Rules> {
41132×
    let mut text = String::new();
42132×
    if let Some(body) = layer(pool, None).await? {
4375×
        text.push_str("# Overall brief\n\n");
4475×
        text.push_str(body.trim_end());
4575×
        text.push('\n');
4675×
    }
47132×
    if let Some(p) = project {
48129×
        if let Some(body) = layer(pool, Some(p)).await? {
4930×
            if !text.is_empty() {
5027×
                text.push('\n');
5127×
            }
5230×
            text.push_str(&format!("# Project brief: {p}\n\n"));
5330×
            text.push_str(body.trim_end());
5430×
            text.push('\n');
5599×
        }
563×
    }
57132×
    let version = blake3::hash(text.as_bytes()).to_hex()[..12].to_string();
58132×
    Ok(Rules { text, version })
59132×
}
60
61
/// The version `agent` last read, if any.
6217×
pub async fn last_read(pool: &sqlx::SqlitePool, agent: &str) -> sqlx::Result<Option<String>> {
6317×
    sqlx::query_scalar("SELECT version FROM rules_read WHERE agent = ?").bind(agent).fetch_optional(pool).await
6417×
}
65
66
/// The line every result begins with while an agent works on stale rules.
674×
pub fn changed_line(read: &str, now: &str) -> String {
684×
    format!("Your brief changed ({read} → {now}): call rules before continuing.")
694×
}
70
71
/// Apply `brief.set`, `rules.read` and `tool.called` to their tables.
7242757×
pub async fn project(conn: &mut sqlx::SqliteConnection, e: &Event) -> sqlx::Result<()> {
7342757×
    let p = e.json();
7442757×
    let agent = e.actor.strip_prefix("agent:");
7542757×
    match e.kind.as_str() {
76
        // Only the owner sets a brief; an agent's key can never sign as `human`.
7742757×
        "brief.set" if e.actor == "human" => {
7816×
            let project = p.get("project").and_then(|v| v.as_str()).unwrap_or("");
7916×
            let layer = if project.is_empty() { "overall" } else { "project" };
8016×
            let text = p.get("text").and_then(|v| v.as_str()).unwrap_or("");
8116×
            if text.trim().is_empty() {
823×
                sqlx::query("DELETE FROM briefs WHERE layer = ? AND project = ?").bind(layer).bind(project).execute(&mut *conn).await?;
83
            } else {
8413×
                sqlx::query("INSERT OR REPLACE INTO briefs (layer, project, text, seq) VALUES (?, ?, ?, ?)")
8513×
                    .bind(layer)
8613×
                    .bind(project)
8713×
                    .bind(text)
8813×
                    .bind(e.seq as i64)
8913×
                    .execute(&mut *conn)
9013×
                    .await?;
91
            }
92
        }
9342741×
        "rules.read" => {
9421×
            if let (Some(a), Some(v)) = (agent, p.get("version").and_then(|v| v.as_str())) {
9521×
                sqlx::query("INSERT INTO rules_read (agent, version, at_ms) VALUES (?, ?, ?) ON CONFLICT(agent) DO UPDATE SET version = excluded.version, at_ms = excluded.at_ms")
9621×
                    .bind(a)
9721×
                    .bind(v)
9821×
                    .bind(e.at_ms as i64)
9921×
                    .execute(&mut *conn)
10021×
                    .await?;
1010×
            }
102
        }
10342720×
        "tool.called" => {
1043419×
            let s = |k: &str| p.get(k).and_then(|v| v.as_str()).map(String::from);
1053321×
            if let (Some(a), Some(tool)) = (agent, s("tool")) {
10649×
                sqlx::query("INSERT OR IGNORE INTO tool_calls (seq, agent, tool, args, result, ms, is_error, read, session, at_ms) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")
10749×
                    .bind(e.seq as i64)
10849×
                    .bind(a)
10949×
                    .bind(tool)
11049×
                    .bind(s("args").unwrap_or_default())
11149×
                    .bind(s("result").unwrap_or_default())
11249×
                    .bind(p.get("ms").and_then(|v| v.as_i64()).unwrap_or(0))
11349×
                    .bind(p.get("is_error").and_then(|v| v.as_bool()).unwrap_or(false))
11449×
                    .bind(p.get("read").and_then(|v| v.as_bool()).unwrap_or(false))
11549×
                    .bind(&e.session)
11649×
                    .bind(e.at_ms as i64)
11749×
                    .execute(&mut *conn)
11849×
                    .await?;
1193272×
            }
120
        }
12139399×
        _ => {}
122
    }
12342757×
    Ok(())
12442757×
}
125
126
#[cfg(test)]
127
mod tests {
128
    use super::*;
129
130
    #[test]
1311×
    fn a_brief_must_fit_in_one_call() {
1321×
        assert!(check("Write for a curious twelve-year-old.").is_ok());
1331×
        assert!(check(&"x".repeat(MAX_BRIEF)).is_ok());
1341×
        assert!(check(&"x".repeat(MAX_BRIEF + 1)).is_err());
1351×
        assert!(changed_line("aaa", "bbb").starts_with("Your brief changed (aaa → bbb)"));
1361×
    }
137
}