b044d909 Andy McCall 1 day ago ↶
1
2
3
4
5
037c45d8 Andy McCall 2 days ago ↶
6
7
use crate :: event:: Event;
8
use anyhow:: { bail, Result } ;
9
b044d909 Andy McCall 1 day ago ↶
10
11
pub const MAX_BRIEF : usize = 64 * 1024 ;
037c45d8 Andy McCall 2 days ago ↶
12
b044d909 Andy McCall 1 day ago ↶
13
037c45d8 Andy McCall 2 days ago ↶
14
# [ derive ( Clone, Debug, PartialEq, Eq ) ]
15
pub struct Rules {
16
pub text : String,
17
18
pub version : String,
19
}
20
b044d909 Andy McCall 1 day ago ↶
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> {
037c45d8 Andy McCall 2 days ago ↶
41
let mut text = String :: new( ) ;
b044d909 Andy McCall 1 day ago ↶
42
if let Some ( body) = layer ( pool, None ) .await? {
43
text.push_str ( " # Overall brief\n \n " ) ;
037c45d8 Andy McCall 2 days ago ↶
44
text.push_str ( body.trim_end ( ) ) ;
45
text.push ( ' \n ' ) ;
46
}
b044d909 Andy McCall 1 day ago ↶
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
}
037c45d8 Andy McCall 2 days ago ↶
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 {
b044d909 Andy McCall 1 day ago ↶
68
format! ( " Your brief changed ({read} → {now} ): call rules before continuing." )
037c45d8 Andy McCall 2 days ago ↶
69
}
70
b044d909 Andy McCall 1 day ago ↶
71
037c45d8 Andy McCall 2 days ago ↶
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 ( ) {
b044d909 Andy McCall 1 day ago ↶
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
}
037c45d8 Andy McCall 2 days ago ↶
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 ]
b044d909 Andy McCall 1 day ago ↶
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)" ) ) ;
037c45d8 Andy McCall 2 days ago ↶
136
}
137
}