1use areev_cal::classify::{classify, StatementClass};
27use areev_cal::{parse, CalExecutor, CalExecutorConfig, CalStoreFacade, AreevFacade};
28use areev_core::error::{AreevError, Hash};
29use areev_core::format::deserialize::DeserializedGrain;
30use areev_core::authz::Verb;
31use areev_core::types::{Fact, Grain, GrainType};
32use areev_store::{Areev, TelemetryMode, OP_FORGET};
33use serde_json::{Map, Value};
34use std::collections::BTreeSet;
35use areev_loop::error::{Error as WErr, Result as WResult};
36use areev_loop::{
37 BudgetUsage, Capabilities, GrainAccess, GrainRecord, GrainSpec, HeadGroup, OmsSubstrate,
38 QueryUsage, ReadOpts, SubstrateRead, TelemetryView,
39};
40
41const LOOP_NS: &str = "areev-loop";
43const STATE_SUBJECT: &str = "__loop_state__";
45const STATE_RELATION: &str = "state";
46const MAX_SCAN: usize = 1_000_000;
48
49fn we(e: AreevError) -> WErr {
50 WErr::Substrate(e.to_string())
51}
52
53fn require_read(f: &AreevFacade, ns: &str) -> WResult<()> {
54 f.authz().check(Verb::Read, ns).map_err(we)
55}
56
57fn may_read(f: &AreevFacade, ns: &str) -> bool {
58 f.authz().allows(Verb::Read, ns)
59}
60
61pub struct AreevSubstrate {
63 facade: AreevFacade,
64}
65
66impl AreevSubstrate {
67 pub fn new(store: Areev, session_ns: Option<String>) -> Self {
70 let ns = session_ns.unwrap_or_else(|| LOOP_NS.to_string());
71 AreevSubstrate {
72 facade: AreevFacade::with_session(store, Some(ns), None),
73 }
74 }
75
76 pub fn into_store(self) -> Areev {
78 self.facade.into_inner()
79 }
80}
81
82pub struct BorrowedSubstrate<'a> {
85 facade: &'a AreevFacade,
86}
87
88impl<'a> BorrowedSubstrate<'a> {
89 pub fn new(facade: &'a AreevFacade) -> Self {
90 BorrowedSubstrate { facade }
91 }
92}
93
94impl AreevSubstrate {
95 fn facade_ref(&self) -> &AreevFacade {
96 &self.facade
97 }
98
99 pub fn facade(&self) -> &AreevFacade {
102 &self.facade
103 }
104}
105
106impl BorrowedSubstrate<'_> {
107 fn facade_ref(&self) -> &AreevFacade {
108 self.facade
109 }
110}
111
112macro_rules! impl_substrate {
115 ($ty:ty) => {
116 impl SubstrateRead for $ty {
117 fn capabilities(&self) -> Capabilities {
118 caps(self.facade_ref())
119 }
120 fn grains_of_type(
121 &self,
122 grain_type: &str,
123 namespace: Option<&str>,
124 opts: ReadOpts,
125 ) -> WResult<Vec<GrainRecord>> {
126 grains_of_type(self.facade_ref(), grain_type, namespace, opts)
127 }
128 fn grain(&self, hash: &str) -> WResult<Option<GrainRecord>> {
129 grain(self.facade_ref(), hash)
130 }
131 fn heads(&self, namespace: Option<&str>) -> WResult<Vec<HeadGroup>> {
132 heads(self.facade_ref(), namespace)
133 }
134 fn telemetry(&self, namespace: Option<&str>) -> WResult<Option<TelemetryView>> {
135 telemetry(self.facade_ref(), namespace)
136 }
137 }
138
139 impl OmsSubstrate for $ty {
140 fn put_grain(&mut self, spec: &GrainSpec) -> WResult<String> {
141 put_grain(self.facade_ref(), spec)
142 }
143 fn supersede(
144 &mut self,
145 target_hash: &str,
146 spec: &GrainSpec,
147 _j: &str,
148 ) -> WResult<String> {
149 supersede_op(self.facade_ref(), target_hash, spec)
150 }
151 fn retract(&mut self, hash: &str, _reason: &str) -> WResult<()> {
152 retract_op(self.facade_ref(), hash)
153 }
154 fn execute_cal(&mut self, cal: &str) -> WResult<Vec<Value>> {
155 execute_cal(self.facade_ref(), cal)
156 }
157 fn validate_cal(&self, cal: &str) -> WResult<()> {
158 validate_cal(cal)
159 }
160 fn load_state(&self) -> WResult<Value> {
161 load_state(self.facade_ref())
162 }
163 fn store_state(&mut self, state: &Value) -> WResult<()> {
164 store_state(self.facade_ref(), state)
165 }
166 fn put_blob(&mut self, bytes: &[u8]) -> WResult<String> {
167 put_blob_op(self.facade_ref(), bytes)
168 }
169 fn get_blob(&mut self, address: &str) -> WResult<Vec<u8>> {
170 get_blob_op(self.facade_ref(), address)
171 }
172 }
173 };
174}
175
176impl_substrate!(AreevSubstrate);
177impl_substrate!(BorrowedSubstrate<'_>);
178
179fn recent_ns(f: &AreevFacade, ns: &str, gt: GrainType) -> WResult<Vec<DeserializedGrain>> {
182 require_read(f, ns)?;
186 f.with_store(|m| m.recent(ns, Some(gt), MAX_SCAN))
187 .map_err(we)
188}
189
190fn all_grains(f: &AreevFacade, gt: GrainType) -> WResult<Vec<DeserializedGrain>> {
192 let ops = f.with_store(|m| m.changes_since(0, MAX_SCAN)).map_err(we)?;
193 let mut seen = BTreeSet::new();
194 let mut out = Vec::new();
195 for op in ops {
196 if op.op == OP_FORGET || !seen.insert(op.hash.to_hex()) {
197 continue;
198 }
199 if let Ok(g) = f.with_store(|m| m.get(&op.hash)) {
200 if g.grain_type == gt && may_read(f, &grain_namespace(&g)) {
201 out.push(g);
202 }
203 }
204 }
205 Ok(out)
206}
207
208fn read_user_type(
209 f: &AreevFacade,
210 gt: GrainType,
211 namespace: Option<&str>,
212 opts: ReadOpts,
213) -> WResult<Vec<GrainRecord>> {
214 let explicit = namespace.is_some();
215 let raw = match namespace {
216 Some(ns) => recent_ns(f, ns, gt)?,
217 None => all_grains(f, gt)?,
218 };
219 let superseded = superseded_set(&raw);
220 let mut out = Vec::new();
221 for g in raw {
222 let ns = grain_namespace(&g);
223 if !explicit && (ns == LOOP_NS || ns.starts_with("agent:")) {
235 continue;
236 }
237 if opts.live_only && superseded.contains(&g.hash.to_hex()) {
238 continue;
239 }
240 let created = grain_created_ms(&g);
241 if opts.since_ms.is_some_and(|s| created < s) {
242 continue;
243 }
244 out.push(map_user_grain(&g, ns, created));
245 }
246 Ok(out)
247}
248
249fn read_loop(
251 f: &AreevFacade,
252 relation: &str,
253 out_type: &str,
254 opts: ReadOpts,
255) -> WResult<Vec<GrainRecord>> {
256 let facts = recent_ns(f, LOOP_NS, GrainType::Fact)?;
257 let mut out = Vec::new();
258 for fact in facts {
259 if fact.get_str("relation") != Some(relation) {
260 continue;
261 }
262 let Some(payload) = fact.get_str("object") else {
263 continue;
264 };
265 let fields: Map<String, Value> = match serde_json::from_str(payload) {
266 Ok(Value::Object(m)) => m,
267 _ => continue,
268 };
269 let created = grain_created_ms(&fact);
270 if opts.since_ms.is_some_and(|s| created < s) {
271 continue;
272 }
273 out.push(loop_record(
274 &fact.hash.to_hex(),
275 out_type,
276 created,
277 fields,
278 ));
279 }
280 Ok(out)
281}
282
283fn caps(f: &AreevFacade) -> Capabilities {
284 Capabilities {
285 forks: true,
286 telemetry: f.with_store(|m| m.telemetry_mode()) != TelemetryMode::Off,
288 embeddings: f.with_store(|m| m.declared_embedding().is_some()),
289 }
290}
291
292fn telemetry(f: &AreevFacade, namespace: Option<&str>) -> WResult<Option<TelemetryView>> {
296 if f.with_store(|m| m.telemetry_mode()) == TelemetryMode::Off {
297 return Ok(None);
298 }
299 if let Some(ns) = namespace {
300 require_read(f, ns)?;
301 }
302 let access = f.with_store(|m| m.telemetry_access_stats(namespace)).map_err(we)?;
303 let queries = f.with_store(|m| m.telemetry_query_stats(namespace)).map_err(we)?;
304 let budget = if may_read(f, "*") {
308 f.with_store(|m| m.telemetry_budget_stats()).map_err(we)?
309 } else {
310 Default::default()
311 };
312 Ok(Some(TelemetryView {
313 access: access
314 .into_iter()
315 .filter(|a| may_read(f, &a.ns))
316 .map(|a| GrainAccess { hash: a.hash, recall_count: a.recall_count, last_ms: a.last_ms })
317 .collect(),
318 queries: queries
319 .into_iter()
320 .filter(|q| may_read(f, &q.ns))
321 .map(|q| QueryUsage {
322 sample: q.sample,
323 run_count: q.run_count,
324 empty_count: q.empty_count,
325 sum_results: q.sum_results,
326 last_ms: q.last_ms,
327 })
328 .collect(),
329 budget: BudgetUsage {
330 sample_count: budget.sample_count,
331 overflow_count: budget.overflow_count,
332 },
333 }))
334}
335
336fn grains_of_type(
337 f: &AreevFacade,
338 grain_type: &str,
339 namespace: Option<&str>,
340 opts: ReadOpts,
341) -> WResult<Vec<GrainRecord>> {
342 match grain_type {
343 "recommendation" => read_loop(f, "loop_recommendation", "recommendation", opts),
344 "fact" => read_user_type(f, GrainType::Fact, namespace, opts),
345 "event" => read_user_type(f, GrainType::Event, namespace, opts),
346 "tool" => read_user_type(f, GrainType::Tool, namespace, opts),
347 "observation" if namespace == Some(LOOP_NS) => {
351 read_loop(f, "loop_audit", "observation", opts)
352 }
353 "observation" => read_user_type(f, GrainType::Observation, namespace, opts),
354 "skill" => read_user_type(f, GrainType::Skill, namespace, opts),
355 "goal" => read_user_type(f, GrainType::Goal, namespace, opts),
356 other => Err(WErr::Substrate(format!("unsupported grain type {other:?}"))),
357 }
358}
359
360fn grain(f: &AreevFacade, hash: &str) -> WResult<Option<GrainRecord>> {
361 let h = Hash::from_hex(hash).map_err(we)?;
362 let g = match f.with_store(|m| m.get(&h)) {
363 Ok(g) => g,
364 Err(AreevError::NotFound(_)) => return Ok(None),
365 Err(e) => return Err(we(e)),
366 };
367 let created = grain_created_ms(&g);
368 let superseded_by = f
374 .with_store(|m| m.supersession_map(&[h]))
375 .map_err(we)?
376 .get(&h)
377 .map(|s| s.to_hex());
378 let ns = grain_namespace(&g);
379 if ns == LOOP_NS {
384 if let Some(rel) = g.get_str("relation") {
385 if let Some(out_type) = loop_relation_type(rel) {
386 require_read(f, LOOP_NS)?;
387 if let Some(Value::Object(fields)) = g
388 .get_str("object")
389 .and_then(|p| serde_json::from_str(p).ok())
390 {
391 let mut rec = loop_record(hash, out_type, created, fields);
392 rec.superseded_by = superseded_by;
393 return Ok(Some(rec));
394 }
395 }
396 }
397 }
398 require_read(f, &ns)?;
399 let mut rec = map_user_grain(&g, ns, created);
400 rec.superseded_by = superseded_by;
401 Ok(Some(rec))
402}
403
404fn heads(f: &AreevFacade, namespace: Option<&str>) -> WResult<Vec<HeadGroup>> {
405 let forks = f.with_store(|m| m.open_forks()).map_err(we)?;
406 Ok(forks
407 .into_iter()
408 .filter(|fg| fg.namespace != LOOP_NS)
409 .filter(|fg| may_read(f, &fg.namespace))
410 .filter(|fg| namespace.is_none_or(|ns| fg.namespace == ns))
411 .map(|fg| HeadGroup {
412 entity: format!("{}/{}", fg.subject, fg.relation),
413 heads: fg.heads.iter().map(|h| h.to_hex()).collect(),
414 })
415 .collect())
416}
417
418const MAX_LOOP_BLOB: usize = 4 * 1024 * 1024;
425
426fn put_blob_op(f: &AreevFacade, bytes: &[u8]) -> WResult<String> {
427 f.authz().check(Verb::Write, LOOP_NS).map_err(we)?;
428 if bytes.len() > MAX_LOOP_BLOB {
429 return Err(WErr::Substrate(format!(
430 "blob is {} bytes (loop cap {MAX_LOOP_BLOB})",
431 bytes.len()
432 )));
433 }
434 f.with_store(|m| m.put_blob(bytes)).map_err(we)
435}
436
437fn get_blob_op(f: &AreevFacade, address: &str) -> WResult<Vec<u8>> {
438 require_read(f, LOOP_NS)?;
439 f.with_store(|m| m.get_blob(address)).map_err(we)
440}
441
442fn put_grain(f: &AreevFacade, spec: &GrainSpec) -> WResult<String> {
443 if spec.grain_type == "fact" {
449 if let (Some(subject), Some(relation), Some(object)) = (
450 spec.fields.get("subject").and_then(Value::as_str),
451 spec.fields.get("relation").and_then(Value::as_str),
452 spec.fields.get("object").and_then(Value::as_str),
453 ) {
454 let mut fact = Fact::new(subject, relation, object)
455 .namespace(LOOP_NS)
456 .confidence(1.0);
457 fact.common.created_at = spec_created_ms(spec);
458 for (k, v) in &spec.fields {
459 if !matches!(k.as_str(), "subject" | "relation" | "object") {
460 fact.common.extra_fields.insert(k.clone(), v.clone());
461 }
462 }
463 return f.with_store(|m| m.add(&fact)).map(|h| h.to_hex()).map_err(we);
464 }
465 }
466 let payload = serde_json::to_string(&spec.fields)
467 .map_err(|e| WErr::Substrate(format!("encode grain: {e}")))?;
468 let mut fact = Fact::new(
469 &unique_subject(&payload),
470 loop_relation(&spec.grain_type),
471 &payload,
472 )
473 .namespace(LOOP_NS)
474 .confidence(1.0);
475 fact.common.created_at = spec_created_ms(spec);
476 f.with_store(|m| m.add(&fact))
477 .map(|h| h.to_hex())
478 .map_err(we)
479}
480
481fn supersede_op(f: &AreevFacade, target_hash: &str, spec: &GrainSpec) -> WResult<String> {
482 let payload = serde_json::to_string(&spec.fields)
483 .map_err(|e| WErr::Substrate(format!("encode grain: {e}")))?;
484 let old = Hash::from_hex(target_hash).map_err(we)?;
485 let mut fact = Fact::new(
486 &unique_subject(&payload),
487 loop_relation(&spec.grain_type),
488 &payload,
489 )
490 .namespace(LOOP_NS);
491 fact.common.created_at = spec_created_ms(spec);
492 f.with_store(|m| m.supersede(&old, &mut fact))
493 .map(|h| h.to_hex())
494 .map_err(we)
495}
496
497fn spec_created_ms(spec: &GrainSpec) -> Option<i64> {
504 spec.fields
505 .get("created_at_ms")
506 .or_else(|| spec.fields.get("at_ms"))
507 .and_then(Value::as_i64)
508}
509
510fn retract_op(f: &AreevFacade, hash: &str) -> WResult<()> {
511 let h = Hash::from_hex(hash).map_err(we)?;
514 f.with_store(|m| m.forget(&h)).map_err(we)
515}
516
517fn execute_cal(f: &AreevFacade, cal: &str) -> WResult<Vec<Value>> {
518 let mut rows = Vec::new();
523 for line in cal.lines() {
524 let line = line.trim();
525 if line.is_empty() {
526 continue;
527 }
528 let (keyword, rest) = split_keyword(line);
529 match keyword.to_ascii_uppercase().as_str() {
530 "FORGET" => {
531 let h = Hash::from_hex(rest.trim()).map_err(we)?;
532 f.cal_delete(&h, Some("areev-loop apply")).map_err(we)?;
535 }
536 "ADD" => {
537 let (gtype, fields) = parse_type_and_json(rest)?;
538 let h = f.cal_add(>ype, &fields).map_err(we)?;
539 rows.push(serde_json::json!({ "hash": h.to_hex() }));
540 }
541 "SUPERSEDE" => {
542 let (target, after) = rest
543 .split_once(" WITH ")
544 .ok_or_else(|| WErr::CalUnsupported(format!("malformed SUPERSEDE: {line}")))?;
545 let (gtype, fields) = parse_type_and_json(after)?;
546 let old = Hash::from_hex(target.trim()).map_err(we)?;
547 let h = f.cal_supersede(&old, >ype, &fields).map_err(we)?;
548 rows.push(serde_json::json!({ "hash": h.to_hex() }));
549 }
550 _ => {
551 let parsed = parse(line).map_err(|e| WErr::CalUnsupported(e.to_string()))?;
558 if classify(&parsed.statement) == StatementClass::Destructive {
559 return Err(WErr::CalUnsupported(format!(
560 "destructive CAL beyond FORGET <hash> is not executable through the loop substrate: {line}"
561 )));
562 }
563 let ex = CalExecutor::new(CalExecutorConfig::default());
564 let res = ex
565 .execute(line, f)
566 .map_err(|e| WErr::CalUnsupported(e.to_string()))?;
567 let payload = serde_json::to_value(&res.result)
568 .map_err(|e| WErr::Substrate(format!("encode CAL result: {e}")))?;
569 let mut hs = Vec::new();
570 collect_hashes(&payload, &mut hs);
571 rows.extend(hs.into_iter().map(|h| serde_json::json!({ "hash": h })));
572 }
573 }
574 }
575 Ok(rows)
576}
577
578fn validate_cal(cal: &str) -> WResult<()> {
579 for line in cal.lines() {
580 let line = line.trim();
581 if line.is_empty() {
582 continue;
583 }
584 let (keyword, rest) = split_keyword(line);
585 match keyword.to_ascii_uppercase().as_str() {
586 "ADD" | "SUPERSEDE" => {}
587 "FORGET" => {
590 Hash::from_hex(rest.trim()).map_err(|_| {
591 WErr::CalUnsupported(format!(
592 "loop FORGET takes a single grain hash: {line}"
593 ))
594 })?;
595 }
596 _ => {
597 let parsed = parse(line).map_err(|e| WErr::CalUnsupported(e.to_string()))?;
598 if classify(&parsed.statement) == StatementClass::Destructive {
602 return Err(WErr::CalUnsupported(format!(
603 "destructive CAL beyond FORGET <hash> is not executable through the loop substrate: {line}"
604 )));
605 }
606 }
607 }
608 }
609 Ok(())
610}
611
612fn load_state(f: &AreevFacade) -> WResult<Value> {
613 let head = f
614 .with_store(|m| m.latest(LOOP_NS, STATE_SUBJECT, STATE_RELATION))
615 .map_err(we)?;
616 match head.as_ref().and_then(|g| g.get_str("object")) {
617 Some(json) => serde_json::from_str(json)
618 .map_err(|e| WErr::Substrate(format!("decode loop state: {e}"))),
619 None => Ok(Value::Null),
620 }
621}
622
623fn store_state(f: &AreevFacade, state: &Value) -> WResult<()> {
624 let json = serde_json::to_string(state)
625 .map_err(|e| WErr::Substrate(format!("encode loop state: {e}")))?;
626 let existing = f
627 .with_store(|m| m.latest(LOOP_NS, STATE_SUBJECT, STATE_RELATION))
628 .map_err(we)?;
629 f.with_store(|m| {
630 let mut fact = Fact::new(STATE_SUBJECT, STATE_RELATION, &json).namespace(LOOP_NS);
631 match &existing {
632 Some(g) => m.supersede(&g.hash, &mut fact).map(|_| ()),
633 None => m.add(&fact).map(|_| ()),
634 }
635 })
636 .map_err(we)
637}
638
639fn loop_relation(grain_type: &str) -> &'static str {
642 match grain_type {
643 "recommendation" => "loop_recommendation",
644 _ => "loop_audit", }
646}
647
648fn loop_relation_type(relation: &str) -> Option<&'static str> {
649 match relation {
650 "loop_recommendation" => Some("recommendation"),
651 "loop_audit" => Some("observation"),
652 _ => None,
653 }
654}
655
656fn unique_subject(payload: &str) -> String {
659 use std::hash::{Hash as _, Hasher};
660 let mut h = std::collections::hash_map::DefaultHasher::new();
661 payload.hash(&mut h);
662 format!("w{:016x}", h.finish())
663}
664
665fn grain_namespace(g: &DeserializedGrain) -> String {
666 g.get_str("namespace").unwrap_or("").to_string()
667}
668
669fn grain_created_ms(g: &DeserializedGrain) -> i64 {
670 g.get_i64("created_at")
671 .unwrap_or_else(|| g.header.created_at_sec as i64 * 1000)
672}
673
674fn map_user_grain(g: &DeserializedGrain, namespace: String, created_ms: i64) -> GrainRecord {
675 GrainRecord {
676 hash: g.hash.to_hex(),
677 grain_type: g.grain_type.as_str().to_string(),
678 namespace,
679 created_at_ms: created_ms,
680 valid_to_ms: g.get_i64("valid_to"),
681 superseded_by: None,
682 fields: g.fields.clone().into_iter().collect(),
683 }
684}
685
686fn loop_record(
687 hash: &str,
688 grain_type: &str,
689 created_ms: i64,
690 fields: Map<String, Value>,
691) -> GrainRecord {
692 let valid_to_ms = fields.get("valid_to_ms").and_then(Value::as_i64);
693 GrainRecord {
694 hash: hash.to_string(),
695 grain_type: grain_type.to_string(),
696 namespace: LOOP_NS.to_string(),
697 created_at_ms: created_ms,
698 valid_to_ms,
699 superseded_by: None,
700 fields,
701 }
702}
703
704fn superseded_set(grains: &[DeserializedGrain]) -> BTreeSet<String> {
706 let mut set = BTreeSet::new();
707 for g in grains {
708 for parent in derived_parents(g) {
709 set.insert(parent);
710 }
711 }
712 set
713}
714
715fn derived_parents(g: &DeserializedGrain) -> Vec<String> {
716 match g.fields.get("derived_from") {
717 Some(Value::String(s)) => vec![s.clone()],
718 Some(Value::Array(a)) => a
719 .iter()
720 .filter_map(|v| v.as_str().map(str::to_string))
721 .collect(),
722 _ => Vec::new(),
723 }
724}
725
726fn split_keyword(line: &str) -> (&str, &str) {
727 match line.split_once(char::is_whitespace) {
728 Some((k, rest)) => (k, rest.trim_start()),
729 None => (line, ""),
730 }
731}
732
733fn parse_type_and_json(s: &str) -> WResult<(String, Map<String, Value>)> {
735 let brace = s
736 .find('{')
737 .ok_or_else(|| WErr::CalUnsupported(format!("missing JSON object in {s:?}")))?;
738 let gtype = s[..brace].trim().to_string();
739 if gtype.is_empty() {
740 return Err(WErr::CalUnsupported(format!("missing grain type in {s:?}")));
741 }
742 let value: Value = serde_json::from_str(s[brace..].trim())
743 .map_err(|e| WErr::CalUnsupported(format!("bad JSON in {s:?}: {e}")))?;
744 match value {
745 Value::Object(m) => Ok((gtype, m)),
746 _ => Err(WErr::CalUnsupported(format!("JSON not an object in {s:?}"))),
747 }
748}
749
750fn collect_hashes(v: &Value, out: &mut Vec<String>) {
751 match v {
752 Value::Object(m) => {
753 for (k, val) in m {
754 if (k == "hash" || k == "new_hash") && val.is_string() {
755 out.push(val.as_str().unwrap().to_string());
756 } else {
757 collect_hashes(val, out);
758 }
759 }
760 }
761 Value::Array(a) => a.iter().for_each(|x| collect_hashes(x, out)),
762 _ => {}
763 }
764}