//! Beacon (spec §12): the brief an agent receives, compiled from the //! layers the owner has set (overall, then the agent's project), each under //! a heading naming it, with no other change; the version is the BLAKE3 of //! that text. Briefs are `brief.set` events signed by the owner; nothing is //! read from a file. use crate::event::Event; use anyhow::{bail, Result}; /// A brief over this is refused (spec §12.1). pub const MAX_BRIEF: usize = 64 * 1024; /// A compiled brief and its version. #[derive(Clone, Debug, PartialEq, Eq)] pub struct Rules { pub text: String, /// The first 12 hex characters of BLAKE3(text). pub version: String, } /// Check a brief's text before it is signed. pub fn check(text: &str) -> Result<()> { if text.len() > MAX_BRIEF { bail!("a brief is at most 64 KiB, so an agent can read it in one call ({} bytes)", text.len()); } Ok(()) } /// The text of one layer, if set. pub async fn layer(pool: &sqlx::SqlitePool, project: Option<&str>) -> Result> { let (layer, p) = match project { Some(p) => ("project", p), None => ("overall", ""), }; Ok(sqlx::query_scalar("SELECT text FROM briefs WHERE layer = ? AND project = ?").bind(layer).bind(p).fetch_optional(pool).await?) } /// The brief for an agent in `project` (none for the owner: the overall /// brief alone). pub async fn compile(pool: &sqlx::SqlitePool, project: Option<&str>) -> Result { let mut text = String::new(); if let Some(body) = layer(pool, None).await? { text.push_str("# Overall brief\n\n"); text.push_str(body.trim_end()); text.push('\n'); } if let Some(p) = project { if let Some(body) = layer(pool, Some(p)).await? { if !text.is_empty() { text.push('\n'); } text.push_str(&format!("# Project brief: {p}\n\n")); text.push_str(body.trim_end()); text.push('\n'); } } let version = blake3::hash(text.as_bytes()).to_hex()[..12].to_string(); Ok(Rules { text, version }) } /// The version `agent` last read, if any. pub async fn last_read(pool: &sqlx::SqlitePool, agent: &str) -> sqlx::Result> { sqlx::query_scalar("SELECT version FROM rules_read WHERE agent = ?").bind(agent).fetch_optional(pool).await } /// The line every result begins with while an agent works on stale rules. pub fn changed_line(read: &str, now: &str) -> String { format!("Your brief changed ({read} → {now}): call rules before continuing.") } /// Apply `brief.set`, `rules.read` and `tool.called` to their tables. pub async fn project(conn: &mut sqlx::SqliteConnection, e: &Event) -> sqlx::Result<()> { let p = e.json(); let agent = e.actor.strip_prefix("agent:"); match e.kind.as_str() { // Only the owner sets a brief; an agent's key can never sign as `human`. "brief.set" if e.actor == "human" => { let project = p.get("project").and_then(|v| v.as_str()).unwrap_or(""); let layer = if project.is_empty() { "overall" } else { "project" }; let text = p.get("text").and_then(|v| v.as_str()).unwrap_or(""); if text.trim().is_empty() { sqlx::query("DELETE FROM briefs WHERE layer = ? AND project = ?").bind(layer).bind(project).execute(&mut *conn).await?; } else { sqlx::query("INSERT OR REPLACE INTO briefs (layer, project, text, seq) VALUES (?, ?, ?, ?)") .bind(layer) .bind(project) .bind(text) .bind(e.seq as i64) .execute(&mut *conn) .await?; } } "rules.read" => { if let (Some(a), Some(v)) = (agent, p.get("version").and_then(|v| v.as_str())) { 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") .bind(a) .bind(v) .bind(e.at_ms as i64) .execute(&mut *conn) .await?; } } "tool.called" => { let s = |k: &str| p.get(k).and_then(|v| v.as_str()).map(String::from); if let (Some(a), Some(tool)) = (agent, s("tool")) { sqlx::query("INSERT OR IGNORE INTO tool_calls (seq, agent, tool, args, result, ms, is_error, read, session, at_ms) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)") .bind(e.seq as i64) .bind(a) .bind(tool) .bind(s("args").unwrap_or_default()) .bind(s("result").unwrap_or_default()) .bind(p.get("ms").and_then(|v| v.as_i64()).unwrap_or(0)) .bind(p.get("is_error").and_then(|v| v.as_bool()).unwrap_or(false)) .bind(p.get("read").and_then(|v| v.as_bool()).unwrap_or(false)) .bind(&e.session) .bind(e.at_ms as i64) .execute(&mut *conn) .await?; } } _ => {} } Ok(()) } #[cfg(test)] mod tests { use super::*; #[test] fn a_brief_must_fit_in_one_call() { assert!(check("Write for a curious twelve-year-old.").is_ok()); assert!(check(&"x".repeat(MAX_BRIEF)).is_ok()); assert!(check(&"x".repeat(MAX_BRIEF + 1)).is_err()); assert!(changed_line("aaa", "bbb").starts_with("Your brief changed (aaa → bbb)")); } }