use crate::value::VmValue;
use super::answer::{Answer, AnswerBody};
#[derive(Clone, Debug)]
pub struct Outcome {
pub kind: &'static str,
value: VmValue,
}
impl Outcome {
pub(super) fn from_recorded(value: VmValue) -> Result<Self, crate::VmError> {
if !harn_kernel::type_contract::matches_manifest_type(
&value,
&harn_builtin_meta::predicate::EVALUATION_OUTCOME,
) {
return Err(crate::VmError::Runtime(
"evaluation tape outcome violates its closed contract".into(),
));
}
let kind = match value
.as_dict()
.and_then(|fields| fields.get("kind"))
.and_then(|value| match value {
VmValue::String(text) => Some(text.as_str()),
_ => None,
}) {
Some("answered") => "answered",
Some("low_confidence") => "low_confidence",
Some("refused") => "refused",
Some("budget_cut") => "budget_cut",
Some("unavailable") => "unavailable",
Some("replay_mismatch") => "replay_mismatch",
Some("cancelled") => "cancelled",
Some("state_too_large") => "state_too_large",
Some("question_invalid") => "question_invalid",
Some("rate_limited") => "rate_limited",
Some("overloaded") => "overloaded",
_ => {
return Err(crate::VmError::Runtime(
"unsupported evaluation tape outcome".into(),
))
}
};
Ok(Self { kind, value })
}
pub fn into_value(self) -> VmValue {
self.value
}
}
fn arm(kind: &'static str, receipt: &str, fields: Vec<(&str, VmValue)>) -> Outcome {
let mut entries = vec![("kind", VmValue::String(kind.into()))];
entries.extend(fields);
entries.push(("receipt", VmValue::String(receipt.into())));
Outcome {
kind,
value: VmValue::dict(entries),
}
}
fn answer_map(answers: &[Answer]) -> VmValue {
VmValue::dict(
answers
.iter()
.map(|answer| (answer.question_id.as_str(), answer.to_vm_value()))
.collect::<Vec<_>>(),
)
}
pub fn answered(receipt: &str, answers: &[Answer]) -> Outcome {
arm("answered", receipt, vec![("value", answer_map(answers))])
}
pub fn low_confidence(receipt: &str, answers: &[Answer], threshold: f64) -> Outcome {
let under: Vec<VmValue> = answers
.iter()
.filter(|answer| !answer.meets(threshold))
.map(|answer| VmValue::String(answer.question_id.as_str().into()))
.collect();
arm(
"low_confidence",
receipt,
vec![
("candidates", answer_map(answers)),
("threshold", VmValue::Float(threshold)),
("question_ids", VmValue::List(under.into())),
],
)
}
pub fn refused(receipt: &str, reason: &str, diagnostic: &str) -> Outcome {
arm(
"refused",
receipt,
vec![
("reason", VmValue::String(reason.into())),
("diagnostic", VmValue::String(diagnostic.into())),
],
)
}
pub fn unavailable(receipt: &str, reason: &str) -> Outcome {
arm(
"unavailable",
receipt,
vec![("reason", VmValue::String(reason.into()))],
)
}
pub fn budget_cut(receipt: &str, limit: &str, requested: f64, remaining: f64) -> Outcome {
arm(
"budget_cut",
receipt,
vec![
("limit", VmValue::String(limit.into())),
("requested", VmValue::Float(requested)),
("remaining", VmValue::Float(remaining)),
],
)
}
pub fn cancelled(receipt: &str, control_event: &str) -> Outcome {
arm(
"cancelled",
receipt,
vec![("control_event", VmValue::String(control_event.into()))],
)
}
pub fn state_too_large(receipt: &str, limit_tokens: usize, estimated_tokens: usize) -> Outcome {
arm(
"state_too_large",
receipt,
vec![
("limit_tokens", VmValue::Int(limit_tokens as i64)),
("estimated_tokens", VmValue::Int(estimated_tokens as i64)),
],
)
}
pub fn question_invalid(receipt: &str, question: &str, reason: &str) -> Outcome {
arm(
"question_invalid",
receipt,
vec![
("question", VmValue::String(question.into())),
("reason", VmValue::String(reason.into())),
],
)
}
pub fn rate_limited(receipt: &str, retry_after_ms: Option<u64>) -> Outcome {
let mut fields = Vec::new();
if let Some(retry_after_ms) = retry_after_ms {
fields.push(("retry_after_ms", VmValue::Int(retry_after_ms as i64)));
}
arm("rate_limited", receipt, fields)
}
pub fn overloaded(receipt: &str) -> Outcome {
arm("overloaded", receipt, Vec::new())
}
pub fn project_to_predicate(outcome: Outcome, answers: &[Answer], threshold: f64) -> Outcome {
let verdict = |answer: &Answer| match &answer.body {
AnswerBody::Boolean { verdict, .. } => Some(VmValue::dict(vec![
("verdict", VmValue::Bool(*verdict)),
("confidence", VmValue::Float(answer.confidence)),
("evidence", VmValue::String(answer.evidence.as_str().into())),
])),
_ => None,
};
let receipt = receipt_of(&outcome);
match outcome.kind {
"answered" => match answers.first().and_then(verdict) {
Some(value) => arm("verdict", &receipt, vec![("value", value)]),
None => refused(
&receipt,
"schema_invalid",
"predicate site did not receive a boolean answer",
),
},
"low_confidence" => match answers.first().and_then(verdict) {
Some(value) => arm(
"low_confidence",
&receipt,
vec![
("candidate", value),
("threshold", VmValue::Float(threshold)),
],
),
None => refused(
&receipt,
"schema_invalid",
"predicate site did not receive a boolean answer",
),
},
_ => outcome,
}
}
fn receipt_of(outcome: &Outcome) -> String {
outcome
.value
.as_dict()
.and_then(|fields| fields.get("receipt"))
.map(|receipt| receipt.as_str_cow().into_owned())
.unwrap_or_default()
}