areev_loop_adapter/decide.rs
1//! The bridge from Areev's decision seam to the loop's
2//! (`docs/decision-model-proposal.md` §4, rows E1–E3).
3//!
4//! `areev_loop` has no Areev dependencies, so it defines its own minimal
5//! [`areev_loop::DecideBackend`] over the proposal's WIRE JSON. This adapter
6//! implements it over any [`areev_core::decide::DecisionBackend`] — a
7//! provider chain from `areev_llm::decide::resolve_chain`, a command backend,
8//! or a test fake — so a host installs its chain with
9//! `engine.with_decider(Box::new(LoopDecider(chain)))`.
10
11use areev_core::decide::{questions_from_wire, DecideRequest, DecisionBackend};
12use std::sync::Arc;
13
14/// An Areev decision backend, seen by the loop.
15pub struct LoopDecider(pub Arc<dyn DecisionBackend>);
16
17impl areev_loop::DecideBackend for LoopDecider {
18 /// `{"state", "questions"}` in → the backend's validated `Decision` as
19 /// `{model, answers, usage?, provider, calibrated, latency_ms}` out.
20 /// Every failure — a request that does not parse, an invalid question,
21 /// any `DEC-E…` from the backend — is `LOP-E051`, which the loop treats
22 /// as "no contribution" for that stage.
23 fn decide(&self, request_json: &str) -> areev_loop::Result<String> {
24 let err = |m: String| areev_loop::Error::DecideBackend(m);
25 let v: serde_json::Value =
26 serde_json::from_str(request_json).map_err(|e| err(format!("request is not JSON: {e}")))?;
27 let questions = questions_from_wire(v.get("questions").unwrap_or(&serde_json::Value::Null))
28 .map_err(|e| err(e.to_string()))?;
29 let state = v.get("state").cloned().unwrap_or(serde_json::Value::Null);
30 let decision = self
31 .0
32 .decide(&DecideRequest::new(state, questions))
33 .map_err(|e| err(e.to_string()))?;
34 Ok(decision.to_json().to_string())
35 }
36
37 fn calibrated(&self) -> bool {
38 self.0.calibrated()
39 }
40
41 fn describe(&self) -> String {
42 self.0.describe()
43 }
44}