| 1 |
|
| 2 |
|
| 3 |
|
| 4 |
|
| 5 |
|
| 6 |
|
| 7 |
use crate::event::Event; |
| 8 |
use anyhow::{bail, Result}; |
| 9 |
|
| 10 |
|
| 11 |
pub const MAX_BRIEF: usize = 64 * 1024; |
| 12 |
|
| 13 |
|
| 14 |
#[derive(Clone, Debug, PartialEq, Eq)] |
| 15 |
pub struct Rules { |
| 16 |
pub text: String, |
| 17 |
|
| 18 |
pub version: String, |
| 19 |
} |
| 20 |
|
| 21 |
|
| 22 |
pub fn check(text: &str) -> Result<()> { |
| 23 |
if text.len() > MAX_BRIEF { |
| 24 |
bail!("a brief is at most 64 KiB, so an agent can read it in one call ({} bytes)", text.len()); |
| 25 |
} |
| 26 |
Ok(()) |
| 27 |
} |
| 28 |
|
| 29 |
|
| 30 |
pub async fn layer(pool: &sqlx::SqlitePool, project: Option<&str>) -> Result<Option<String>> { |
| 31 |
let (layer, p) = match project { |
| 32 |
Some(p) => ("project", p), |
| 33 |
None => ("overall", ""), |
| 34 |
}; |
| 35 |
Ok(sqlx::query_scalar("SELECT text FROM briefs WHERE layer = ? AND project = ?").bind(layer).bind(p).fetch_optional(pool).await?) |
| 36 |
} |
| 37 |
|
| 38 |
|
| 39 |
|
| 40 |
pub async fn compile(pool: &sqlx::SqlitePool, project: Option<&str>) -> Result<Rules> { |
| 41 |
let mut text = String::new(); |
| 42 |
if let Some(body) = layer(pool, None).await? { |
| 43 |
text.push_str("# Overall brief\n\n"); |
| 44 |
text.push_str(body.trim_end()); |
| 45 |
text.push('\n'); |
| 46 |
} |
| 47 |
if let Some(p) = project { |
| 48 |
if let Some(body) = layer(pool, Some(p)).await? { |
| 49 |
if !text.is_empty() { |
| 50 |
text.push('\n'); |
| 51 |
} |
| 52 |
text.push_str(&format!("# Project brief: {p}\n\n")); |
| 53 |
text.push_str(body.trim_end()); |
| 54 |
text.push('\n'); |
| 55 |
} |
| 56 |
} |
| 57 |
let version = blake3::hash(text.as_bytes()).to_hex()[..12].to_string(); |
| 58 |
Ok(Rules { text, version }) |
| 59 |
} |
| 60 |
|
| 61 |
|
| 62 |
pub async fn last_read(pool: &sqlx::SqlitePool, agent: &str) -> sqlx::Result<Option<String>> { |
| 63 |
sqlx::query_scalar("SELECT version FROM rules_read WHERE agent = ?").bind(agent).fetch_optional(pool).await |
| 64 |
} |
| 65 |
|
| 66 |
|
| 67 |
pub fn changed_line(read: &str, now: &str) -> String { |
| 68 |
format!("Your brief changed ({read} → {now}): call rules before continuing.") |
| 69 |
} |
| 70 |
|
| 71 |
|
| 72 |
pub async fn project(conn: &mut sqlx::SqliteConnection, e: &Event) -> sqlx::Result<()> { |
| 73 |
let p = e.json(); |
| 74 |
let agent = e.actor.strip_prefix("agent:"); |
| 75 |
match e.kind.as_str() { |
| 76 |
|
| 77 |
"brief.set" if e.actor == "human" => { |
| 78 |
let project = p.get("project").and_then(|v| v.as_str()).unwrap_or(""); |
| 79 |
let layer = if project.is_empty() { "overall" } else { "project" }; |
| 80 |
let text = p.get("text").and_then(|v| v.as_str()).unwrap_or(""); |
| 81 |
if text.trim().is_empty() { |
| 82 |
sqlx::query("DELETE FROM briefs WHERE layer = ? AND project = ?").bind(layer).bind(project).execute(&mut *conn).await?; |
| 83 |
} else { |
| 84 |
sqlx::query("INSERT OR REPLACE INTO briefs (layer, project, text, seq) VALUES (?, ?, ?, ?)") |
| 85 |
.bind(layer) |
| 86 |
.bind(project) |
| 87 |
.bind(text) |
| 88 |
.bind(e.seq as i64) |
| 89 |
.execute(&mut *conn) |
| 90 |
.await?; |
| 91 |
} |
| 92 |
} |
| 93 |
"rules.read" => { |
| 94 |
if let (Some(a), Some(v)) = (agent, p.get("version").and_then(|v| v.as_str())) { |
| 95 |
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") |
| 96 |
.bind(a) |
| 97 |
.bind(v) |
| 98 |
.bind(e.at_ms as i64) |
| 99 |
.execute(&mut *conn) |
| 100 |
.await?; |
| 101 |
} |
| 102 |
} |
| 103 |
"tool.called" => { |
| 104 |
let s = |k: &str| p.get(k).and_then(|v| v.as_str()).map(String::from); |
| 105 |
if let (Some(a), Some(tool)) = (agent, s("tool")) { |
| 106 |
sqlx::query("INSERT OR IGNORE INTO tool_calls (seq, agent, tool, args, result, ms, is_error, read, session, at_ms) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)") |
| 107 |
.bind(e.seq as i64) |
| 108 |
.bind(a) |
| 109 |
.bind(tool) |
| 110 |
.bind(s("args").unwrap_or_default()) |
| 111 |
.bind(s("result").unwrap_or_default()) |
| 112 |
.bind(p.get("ms").and_then(|v| v.as_i64()).unwrap_or(0)) |
| 113 |
.bind(p.get("is_error").and_then(|v| v.as_bool()).unwrap_or(false)) |
| 114 |
.bind(p.get("read").and_then(|v| v.as_bool()).unwrap_or(false)) |
| 115 |
.bind(&e.session) |
| 116 |
.bind(e.at_ms as i64) |
| 117 |
.execute(&mut *conn) |
| 118 |
.await?; |
| 119 |
} |
| 120 |
} |
| 121 |
_ => {} |
| 122 |
} |
| 123 |
Ok(()) |
| 124 |
} |
| 125 |
|
| 126 |
#[cfg(test)] |
| 127 |
mod tests { |
| 128 |
use super::*; |
| 129 |
|
| 130 |
#[test] |
| 131 |
fn a_brief_must_fit_in_one_call() { |
| 132 |
assert!(check("Write for a curious twelve-year-old.").is_ok()); |
| 133 |
assert!(check(&"x".repeat(MAX_BRIEF)).is_ok()); |
| 134 |
assert!(check(&"x".repeat(MAX_BRIEF + 1)).is_err()); |
| 135 |
assert!(changed_line("aaa", "bbb").starts_with("Your brief changed (aaa → bbb)")); |
| 136 |
} |
| 137 |
} |