use mentra::{TerminalOutputReservation as MentraOutputReservation, TerminalOutputSpec};
use serde_json::Value;
use super::{RunError, RunReport};
#[derive(Debug, Clone, PartialEq)]
pub struct OutputSpec {
pub name: String,
pub description: String,
pub schema: Value,
pub keeps_tools: bool,
}
impl OutputSpec {
pub fn new(name: impl Into<String>, description: impl Into<String>, schema: Value) -> Self {
Self {
name: name.into(),
description: description.into(),
schema,
keeps_tools: false,
}
}
pub fn with_tools(self) -> Self {
Self {
keeps_tools: true,
..self
}
}
pub fn reserve(self) -> OutputReservation {
OutputReservation {
inner: self.into_terminal_spec().reserve(),
}
}
pub(crate) fn into_terminal_spec(self) -> TerminalOutputSpec {
let Self {
name,
description,
schema,
keeps_tools,
} = self;
let spec = TerminalOutputSpec::new(name, description, schema);
if keeps_tools { spec.with_tools() } else { spec }
}
}
#[derive(Debug)]
pub struct OutputReservation {
inner: MentraOutputReservation,
}
impl OutputReservation {
pub fn tool_name(&self) -> &str {
self.inner.tool_name()
}
pub(crate) fn into_inner(self) -> MentraOutputReservation {
self.inner
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum OutputDecision {
Accept(Value),
Reject(String),
}
#[derive(Debug)]
pub enum OutputAttempt<T> {
Accepted(T),
Mismatch(serde_json::Error),
Missing,
}
#[derive(Debug)]
pub struct OutputAttemptReport<T, S> {
pub output: OutputAttempt<T>,
pub report: RunReport<S>,
}
#[derive(Debug)]
pub struct OutputReport<T, S> {
pub value: T,
pub report: RunReport<S>,
}
#[derive(Debug)]
pub struct OutputFailure<S> {
pub error: RunError,
pub report: Option<RunReport<S>>,
}
impl<S> From<RunError> for OutputFailure<S> {
fn from(error: RunError) -> Self {
Self {
error,
report: None,
}
}
}
impl<S> From<OutputFailure<S>> for RunError {
fn from(failure: OutputFailure<S>) -> Self {
failure.error
}
}
impl<S> std::fmt::Display for OutputFailure<S> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.error.fmt(f)
}
}
impl<S: std::fmt::Debug> std::error::Error for OutputFailure<S> {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
Some(&self.error)
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn spec() -> OutputSpec {
OutputSpec::new(
"report",
"the verdict you reached on the last turn",
json!({
"type": "object",
"properties": {
"verdict": { "type": "string", "description": "ship or hold" }
},
"required": ["verdict"]
}),
)
}
#[test]
fn a_spec_reaches_mentra_as_the_caller_wrote_it() {
let terminal = spec().into_terminal_spec();
assert_eq!(terminal.tool_name, "report");
assert_eq!(
terminal.description,
"the verdict you reached on the last turn"
);
assert_eq!(
terminal.schema["properties"]["verdict"]["description"],
"ship or hold"
);
}
#[test]
fn a_spec_is_a_value_a_caller_can_keep_and_reuse() {
let template = spec();
assert_eq!(template.clone(), template);
}
#[test]
fn a_shaping_turn_is_what_a_caller_gets_without_asking_for_more() {
assert!(!spec().keeps_tools);
assert!(!spec().into_terminal_spec().keeps_tools);
}
#[test]
fn asking_for_the_toolset_survives_the_trip_to_mentra() {
let terminal = spec().with_tools().into_terminal_spec();
assert!(terminal.keeps_tools);
assert_eq!(terminal.tool_name, "report", "the rest of the spec travels");
}
#[test]
fn asking_for_the_toolset_leaves_the_caller_a_spec_to_reuse() {
let template = spec().with_tools();
assert_eq!(template.clone(), template);
assert_ne!(template, spec(), "and it is not the shaping spec");
}
fn a_mismatch() -> serde_json::Error {
serde_json::from_str::<u32>("\"not a number\"").expect_err("a mismatch")
}
#[test]
fn the_error_a_caller_reaches_through_question_mark_is_the_one_it_always_got() {
let failure: OutputFailure<()> = OutputFailure {
error: RunError::OutputMismatch(a_mismatch()),
report: None,
};
assert!(matches!(
RunError::from(failure),
RunError::OutputMismatch(_)
));
}
#[test]
fn a_failure_with_no_turn_behind_it_carries_no_report() {
let failure: OutputFailure<()> = RunError::EmptyPrompt.into();
assert!(failure.report.is_none());
assert!(matches!(failure.error, RunError::EmptyPrompt));
}
#[test]
fn a_failure_reads_as_the_error_it_carries() {
let error = RunError::OutputMismatch(a_mismatch());
let message = error.to_string();
let failure: OutputFailure<()> = OutputFailure {
error,
report: None,
};
assert_eq!(failure.to_string(), message);
}
}