use std::process::{ExitStatus, Output};
use anyhow::{Result, anyhow};
use super::said;
use crate::interrupt;
mod stream;
pub(super) use stream::Stream;
mod models;
mod security;
pub use security::SafeguardRefusal;
use security::Security;
pub struct Interpretation {
cli: &'static str,
decoder: Box<dyn Decoder>,
retained: Option<Box<dyn Retained>>,
security: Option<Security>,
models: models::Models,
}
pub struct Completion {
pub report: Option<Report>,
pub outcome: Result<Ended>,
}
#[derive(Default)]
pub struct Report {
pub completion_progress: Vec<String>,
pub warnings: Vec<String>,
pub summary: Option<String>,
pub models: Vec<String>,
}
impl Report {
pub(super) fn set_model_progress(&mut self) {
self.completion_progress = self
.models
.iter()
.map(|model| format!("Model: {model}"))
.collect();
}
}
#[derive(Debug)]
pub struct Ended {
pub session_id: Option<String>,
pub killed: Vec<String>,
pub final_message: Option<String>,
}
impl Ended {
pub fn killed_work(&self) -> Vec<&str> {
self.killed.iter().map(String::as_str).collect()
}
}
pub(super) enum TurnOutcome {
NotFailed,
Failed,
}
impl TurnOutcome {
pub(super) fn from_failed(failed: bool) -> Self {
if failed {
Self::Failed
} else {
Self::NotFailed
}
}
}
pub(super) struct Facts {
pub report: Report,
pub ended: Ended,
pub outcome: TurnOutcome,
pub diagnostic: Option<String>,
}
pub(super) trait Decoder: Send {
fn condense(&mut self, raw: &str) -> StreamUpdate;
fn complete(self: Box<Self>) -> Facts;
}
#[derive(Default)]
pub(super) struct StreamUpdate {
pub progress: Vec<String>,
pub evidence: Vec<Evidence>,
}
pub(super) enum Evidence {
ObservedModel { name: String, scope: ModelScope },
RequestedModelFallback,
SafeguardRefusal(SafeguardRefusal),
}
pub(super) enum ModelScope {
MainLoop,
AttributionOnly,
}
pub(super) trait Retained: Send {
fn reconcile(self: Box<Self>, facts: &mut Facts) -> Result<()>;
}
enum Failure {
Execution(anyhow::Error),
Rejected {
status: ExitStatus,
diagnostic: Option<String>,
refusal: Option<SafeguardRefusal>,
},
}
impl Failure {
fn session_error(self, cli: &str) -> anyhow::Error {
match self {
Self::Execution(error) => error,
Self::Rejected {
status,
diagnostic,
refusal,
} => {
let ended = if status.success() {
"'s turn failed".to_string()
} else {
format!(
" exited {}",
status
.code()
.map_or("by signal".to_string(), |code| code.to_string())
)
};
let error = match diagnostic {
Some(error) => anyhow!("{cli}{ended}: {error}"),
None => anyhow!("{cli}{ended}"),
};
match refusal {
Some(refusal) => error.context(refusal),
None => error,
}
}
}
}
}
impl Interpretation {
pub(super) fn new(cli: &'static str, decoder: Box<dyn Decoder>) -> Self {
Self {
cli,
decoder,
retained: None,
security: None,
models: models::Models::default(),
}
}
pub(super) fn with_retained(mut self, retained: Box<dyn Retained>) -> Self {
self.retained = Some(retained);
self
}
pub fn for_security(mut self, requested_model: Option<&str>) -> Self {
self.security = Some(Security::new(requested_model));
self
}
pub fn observed_models(&self) -> Vec<String> {
self.models.names().to_vec()
}
pub fn condense(&mut self, raw: &str) -> Vec<String> {
let StreamUpdate {
mut progress,
evidence,
} = self.decoder.condense(raw);
for fact in evidence {
if let Evidence::ObservedModel { name, .. } = &fact
&& !self.models.include(name)
{
continue;
}
if let Some(security) = &mut self.security
&& let Some(line) = security.apply(&fact)
{
progress.push(line);
}
}
progress
}
pub fn finish(self, execution: Result<ExitStatus>) -> Completion {
let cli = self.cli;
let (report, outcome) = self.complete(execution);
Completion {
report,
outcome: outcome.map_err(|failure| failure.session_error(cli)),
}
}
pub(super) fn check_output(mut self, output: &Output, refusal: &str) -> Result<()> {
for line in String::from_utf8_lossy(&output.stdout).lines() {
self.condense(line);
}
let (_, outcome) = self.complete(Ok(output.status));
outcome.map(drop).map_err(|failure| match failure {
Failure::Execution(error) => error,
Failure::Rejected { diagnostic, .. } => {
anyhow!("{refusal}: {}", diagnostic.unwrap_or_else(|| said(output)))
}
})
}
fn complete(self, execution: Result<ExitStatus>) -> (Option<Report>, Result<Ended, Failure>) {
if let Err(error) = interrupt::check() {
return (None, Err(Failure::Execution(error)));
}
let mut facts = self.decoder.complete();
facts.report.models = self.models.into_names();
let refusal = self.security.and_then(|security| security.refusal);
if refusal.is_some() {
facts.outcome = TurnOutcome::Failed;
}
let recovered = self
.retained
.map_or(Ok(()), |retained| retained.reconcile(&mut facts));
if let Err(error) = interrupt::check() {
return (None, Err(Failure::Execution(error)));
}
let outcome = execution
.and_then(|status| recovered.map(|()| status))
.map_err(Failure::Execution)
.and_then(|status| {
if !status.success() || matches!(facts.outcome, TurnOutcome::Failed) {
Err(Failure::Rejected {
status,
diagnostic: facts.diagnostic,
refusal,
})
} else {
Ok(facts.ended)
}
});
(Some(facts.report), outcome)
}
}