mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-03 01:40:21 +08:00
469 lines
14 KiB
Rust
469 lines
14 KiB
Rust
pub trait AiRuntimeMissDiagnosticPort: Send + Sync {
|
|
type Decision: Send + Sync;
|
|
type Diagnostic: Send;
|
|
|
|
fn build_runtime_miss_diagnostic(
|
|
&self,
|
|
decision: &Self::Decision,
|
|
plan_kind: &str,
|
|
requested_model: Option<&str>,
|
|
reason: &str,
|
|
) -> Self::Diagnostic;
|
|
|
|
fn set_candidate_count(&self, diagnostic: &mut Self::Diagnostic, candidate_count: usize);
|
|
|
|
fn apply_candidate_evaluation_progress(
|
|
&self,
|
|
diagnostic: &mut Self::Diagnostic,
|
|
candidate_count: usize,
|
|
);
|
|
|
|
fn apply_candidate_terminal_plan_reason(
|
|
&self,
|
|
diagnostic: &mut Self::Diagnostic,
|
|
no_plan_reason: &'static str,
|
|
);
|
|
|
|
fn record_candidate_skip_reason(
|
|
&self,
|
|
diagnostic: &mut Self::Diagnostic,
|
|
skip_reason: &'static str,
|
|
);
|
|
|
|
fn set_runtime_miss_diagnostic(&self, trace_id: &str, diagnostic: Self::Diagnostic);
|
|
|
|
fn mutate_runtime_miss_diagnostic<F>(&self, trace_id: &str, apply: F)
|
|
where
|
|
F: FnOnce(&mut Self::Diagnostic) + Send;
|
|
|
|
fn runtime_miss_diagnostic_has_candidate_signal(&self, trace_id: &str) -> bool;
|
|
}
|
|
|
|
pub trait AiRuntimeMissDiagnosticFields {
|
|
fn set_reason(&mut self, reason: String);
|
|
fn set_candidate_count(&mut self, candidate_count: usize);
|
|
fn candidate_count(&self) -> Option<usize>;
|
|
fn skipped_candidate_count(&self) -> Option<usize>;
|
|
fn skip_reason_count(&self, skip_reason: &str) -> usize;
|
|
fn skip_reason_len(&self) -> usize;
|
|
fn record_skip_reason(&mut self, skip_reason: &'static str);
|
|
}
|
|
|
|
pub fn apply_ai_runtime_candidate_evaluation_progress_to_diagnostic<Diagnostic>(
|
|
diagnostic: &mut Diagnostic,
|
|
candidate_count: usize,
|
|
) where
|
|
Diagnostic: AiRuntimeMissDiagnosticFields,
|
|
{
|
|
diagnostic.set_candidate_count(candidate_count);
|
|
diagnostic.set_reason(if candidate_count == 0 {
|
|
"candidate_list_empty".to_string()
|
|
} else {
|
|
"candidate_evaluation_incomplete".to_string()
|
|
});
|
|
}
|
|
|
|
pub fn apply_ai_runtime_candidate_terminal_plan_reason_to_diagnostic<Diagnostic>(
|
|
diagnostic: &mut Diagnostic,
|
|
no_plan_reason: &'static str,
|
|
) where
|
|
Diagnostic: AiRuntimeMissDiagnosticFields,
|
|
{
|
|
let candidate_count = diagnostic.candidate_count().unwrap_or(0);
|
|
let skipped_candidate_count = diagnostic.skipped_candidate_count().unwrap_or(0);
|
|
diagnostic.set_reason(if candidate_count == 0 {
|
|
"candidate_list_empty".to_string()
|
|
} else if skipped_candidate_count >= candidate_count
|
|
&& diagnostic.skip_reason_len() == 1
|
|
&& diagnostic.skip_reason_count("api_key_concurrency_limit_reached") > 0
|
|
{
|
|
"api_key_concurrency_limit_reached".to_string()
|
|
} else if skipped_candidate_count >= candidate_count {
|
|
"all_candidates_skipped".to_string()
|
|
} else {
|
|
no_plan_reason.to_string()
|
|
});
|
|
}
|
|
|
|
pub fn record_ai_runtime_candidate_skip_reason_on_diagnostic<Diagnostic>(
|
|
diagnostic: &mut Diagnostic,
|
|
skip_reason: &'static str,
|
|
) where
|
|
Diagnostic: AiRuntimeMissDiagnosticFields,
|
|
{
|
|
diagnostic.record_skip_reason(skip_reason);
|
|
}
|
|
|
|
pub fn set_ai_runtime_miss_diagnostic_reason<Port>(
|
|
port: &Port,
|
|
trace_id: &str,
|
|
decision: &Port::Decision,
|
|
plan_kind: &str,
|
|
requested_model: Option<&str>,
|
|
reason: &str,
|
|
) where
|
|
Port: AiRuntimeMissDiagnosticPort,
|
|
{
|
|
port.set_runtime_miss_diagnostic(
|
|
trace_id,
|
|
port.build_runtime_miss_diagnostic(decision, plan_kind, requested_model, reason),
|
|
);
|
|
}
|
|
|
|
pub fn build_ai_runtime_execution_exhausted_diagnostic<Port>(
|
|
port: &Port,
|
|
decision: &Port::Decision,
|
|
plan_kind: &str,
|
|
requested_model: Option<&str>,
|
|
candidate_count: usize,
|
|
) -> Port::Diagnostic
|
|
where
|
|
Port: AiRuntimeMissDiagnosticPort,
|
|
{
|
|
let mut diagnostic = port.build_runtime_miss_diagnostic(
|
|
decision,
|
|
plan_kind,
|
|
requested_model,
|
|
"execution_runtime_candidates_exhausted",
|
|
);
|
|
port.set_candidate_count(&mut diagnostic, candidate_count);
|
|
diagnostic
|
|
}
|
|
|
|
pub fn set_ai_runtime_execution_exhausted_diagnostic<Port>(
|
|
port: &Port,
|
|
trace_id: &str,
|
|
decision: &Port::Decision,
|
|
plan_kind: &str,
|
|
requested_model: Option<&str>,
|
|
candidate_count: usize,
|
|
) where
|
|
Port: AiRuntimeMissDiagnosticPort,
|
|
{
|
|
port.set_runtime_miss_diagnostic(
|
|
trace_id,
|
|
build_ai_runtime_execution_exhausted_diagnostic(
|
|
port,
|
|
decision,
|
|
plan_kind,
|
|
requested_model,
|
|
candidate_count,
|
|
),
|
|
);
|
|
}
|
|
|
|
pub fn build_ai_runtime_candidate_evaluation_diagnostic<Port>(
|
|
port: &Port,
|
|
decision: &Port::Decision,
|
|
plan_kind: &str,
|
|
requested_model: Option<&str>,
|
|
candidate_count: usize,
|
|
) -> Port::Diagnostic
|
|
where
|
|
Port: AiRuntimeMissDiagnosticPort,
|
|
{
|
|
let mut diagnostic = port.build_runtime_miss_diagnostic(
|
|
decision,
|
|
plan_kind,
|
|
requested_model,
|
|
"candidate_evaluation_incomplete",
|
|
);
|
|
port.apply_candidate_evaluation_progress(&mut diagnostic, candidate_count);
|
|
diagnostic
|
|
}
|
|
|
|
pub fn set_ai_runtime_candidate_evaluation_diagnostic<Port>(
|
|
port: &Port,
|
|
trace_id: &str,
|
|
decision: &Port::Decision,
|
|
plan_kind: &str,
|
|
requested_model: Option<&str>,
|
|
candidate_count: usize,
|
|
) where
|
|
Port: AiRuntimeMissDiagnosticPort,
|
|
{
|
|
port.set_runtime_miss_diagnostic(
|
|
trace_id,
|
|
build_ai_runtime_candidate_evaluation_diagnostic(
|
|
port,
|
|
decision,
|
|
plan_kind,
|
|
requested_model,
|
|
candidate_count,
|
|
),
|
|
);
|
|
}
|
|
|
|
pub fn apply_ai_runtime_candidate_evaluation_progress<Port>(
|
|
port: &Port,
|
|
trace_id: &str,
|
|
candidate_count: usize,
|
|
) where
|
|
Port: AiRuntimeMissDiagnosticPort,
|
|
{
|
|
port.mutate_runtime_miss_diagnostic(trace_id, |diagnostic| {
|
|
port.apply_candidate_evaluation_progress(diagnostic, candidate_count);
|
|
});
|
|
}
|
|
|
|
pub fn apply_ai_runtime_candidate_evaluation_progress_preserving_candidate_signal<Port>(
|
|
port: &Port,
|
|
trace_id: &str,
|
|
candidate_count: usize,
|
|
) where
|
|
Port: AiRuntimeMissDiagnosticPort,
|
|
{
|
|
let preserve_existing_candidate_signal =
|
|
candidate_count == 0 && port.runtime_miss_diagnostic_has_candidate_signal(trace_id);
|
|
if preserve_existing_candidate_signal {
|
|
return;
|
|
}
|
|
apply_ai_runtime_candidate_evaluation_progress(port, trace_id, candidate_count);
|
|
}
|
|
|
|
pub fn apply_ai_runtime_candidate_terminal_reason<Port>(
|
|
port: &Port,
|
|
trace_id: &str,
|
|
no_plan_reason: &'static str,
|
|
) where
|
|
Port: AiRuntimeMissDiagnosticPort,
|
|
{
|
|
port.mutate_runtime_miss_diagnostic(trace_id, |diagnostic| {
|
|
port.apply_candidate_terminal_plan_reason(diagnostic, no_plan_reason);
|
|
});
|
|
}
|
|
|
|
pub fn record_ai_runtime_candidate_skip_reason<Port>(
|
|
port: &Port,
|
|
trace_id: &str,
|
|
skip_reason: &'static str,
|
|
) where
|
|
Port: AiRuntimeMissDiagnosticPort,
|
|
{
|
|
port.mutate_runtime_miss_diagnostic(trace_id, |diagnostic| {
|
|
port.record_candidate_skip_reason(diagnostic, skip_reason);
|
|
});
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use std::collections::BTreeMap;
|
|
use std::sync::Mutex;
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
struct TestDecision {
|
|
id: &'static str,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
|
struct TestDiagnostic {
|
|
decision_id: String,
|
|
plan_kind: String,
|
|
requested_model: Option<String>,
|
|
reason: String,
|
|
candidate_count: Option<usize>,
|
|
terminal_reason: Option<&'static str>,
|
|
skip_reasons: BTreeMap<&'static str, usize>,
|
|
}
|
|
|
|
#[derive(Default)]
|
|
struct TestPort {
|
|
diagnostics: Mutex<BTreeMap<String, TestDiagnostic>>,
|
|
}
|
|
|
|
impl AiRuntimeMissDiagnosticPort for TestPort {
|
|
type Decision = TestDecision;
|
|
type Diagnostic = TestDiagnostic;
|
|
|
|
fn build_runtime_miss_diagnostic(
|
|
&self,
|
|
decision: &Self::Decision,
|
|
plan_kind: &str,
|
|
requested_model: Option<&str>,
|
|
reason: &str,
|
|
) -> Self::Diagnostic {
|
|
TestDiagnostic {
|
|
decision_id: decision.id.to_string(),
|
|
plan_kind: plan_kind.to_string(),
|
|
requested_model: requested_model.map(str::to_string),
|
|
reason: reason.to_string(),
|
|
..Default::default()
|
|
}
|
|
}
|
|
|
|
fn set_candidate_count(&self, diagnostic: &mut Self::Diagnostic, candidate_count: usize) {
|
|
diagnostic.candidate_count = Some(candidate_count);
|
|
}
|
|
|
|
fn apply_candidate_evaluation_progress(
|
|
&self,
|
|
diagnostic: &mut Self::Diagnostic,
|
|
candidate_count: usize,
|
|
) {
|
|
diagnostic.candidate_count = Some(candidate_count);
|
|
}
|
|
|
|
fn apply_candidate_terminal_plan_reason(
|
|
&self,
|
|
diagnostic: &mut Self::Diagnostic,
|
|
no_plan_reason: &'static str,
|
|
) {
|
|
diagnostic.terminal_reason = Some(no_plan_reason);
|
|
}
|
|
|
|
fn record_candidate_skip_reason(
|
|
&self,
|
|
diagnostic: &mut Self::Diagnostic,
|
|
skip_reason: &'static str,
|
|
) {
|
|
*diagnostic.skip_reasons.entry(skip_reason).or_insert(0) += 1;
|
|
}
|
|
|
|
fn set_runtime_miss_diagnostic(&self, trace_id: &str, diagnostic: Self::Diagnostic) {
|
|
self.diagnostics
|
|
.lock()
|
|
.unwrap()
|
|
.insert(trace_id.to_string(), diagnostic);
|
|
}
|
|
|
|
fn mutate_runtime_miss_diagnostic<F>(&self, trace_id: &str, apply: F)
|
|
where
|
|
F: FnOnce(&mut Self::Diagnostic) + Send,
|
|
{
|
|
let mut diagnostics = self.diagnostics.lock().unwrap();
|
|
let diagnostic = diagnostics.entry(trace_id.to_string()).or_default();
|
|
apply(diagnostic);
|
|
}
|
|
|
|
fn runtime_miss_diagnostic_has_candidate_signal(&self, trace_id: &str) -> bool {
|
|
self.diagnostics
|
|
.lock()
|
|
.unwrap()
|
|
.get(trace_id)
|
|
.is_some_and(|diagnostic| diagnostic.candidate_count.unwrap_or_default() > 0)
|
|
}
|
|
}
|
|
|
|
impl AiRuntimeMissDiagnosticFields for TestDiagnostic {
|
|
fn set_reason(&mut self, reason: String) {
|
|
self.reason = reason;
|
|
}
|
|
|
|
fn set_candidate_count(&mut self, candidate_count: usize) {
|
|
self.candidate_count = Some(candidate_count);
|
|
}
|
|
|
|
fn candidate_count(&self) -> Option<usize> {
|
|
self.candidate_count
|
|
}
|
|
|
|
fn skipped_candidate_count(&self) -> Option<usize> {
|
|
self.skip_reasons.values().copied().sum::<usize>().into()
|
|
}
|
|
|
|
fn skip_reason_count(&self, skip_reason: &str) -> usize {
|
|
self.skip_reasons.get(skip_reason).copied().unwrap_or(0)
|
|
}
|
|
|
|
fn skip_reason_len(&self) -> usize {
|
|
self.skip_reasons.len()
|
|
}
|
|
|
|
fn record_skip_reason(&mut self, skip_reason: &'static str) {
|
|
*self.skip_reasons.entry(skip_reason).or_insert(0) += 1;
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn runtime_miss_builds_and_sets_execution_exhausted_diagnostic() {
|
|
let port = TestPort::default();
|
|
|
|
set_ai_runtime_execution_exhausted_diagnostic(
|
|
&port,
|
|
"trace-a",
|
|
&TestDecision { id: "decision-a" },
|
|
"openai_chat",
|
|
Some("gpt-5"),
|
|
3,
|
|
);
|
|
|
|
let diagnostic = port.diagnostics.lock().unwrap().get("trace-a").cloned();
|
|
assert_eq!(
|
|
diagnostic,
|
|
Some(TestDiagnostic {
|
|
decision_id: "decision-a".to_string(),
|
|
plan_kind: "openai_chat".to_string(),
|
|
requested_model: Some("gpt-5".to_string()),
|
|
reason: "execution_runtime_candidates_exhausted".to_string(),
|
|
candidate_count: Some(3),
|
|
..Default::default()
|
|
})
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn runtime_miss_preserves_candidate_signal_and_records_terminal_updates() {
|
|
let port = TestPort::default();
|
|
apply_ai_runtime_candidate_evaluation_progress(&port, "trace-a", 2);
|
|
|
|
apply_ai_runtime_candidate_evaluation_progress_preserving_candidate_signal(
|
|
&port, "trace-a", 0,
|
|
);
|
|
apply_ai_runtime_candidate_terminal_reason(&port, "trace-a", "no_local_sync_plans");
|
|
record_ai_runtime_candidate_skip_reason(&port, "trace-a", "transport_missing");
|
|
|
|
let diagnostic = port.diagnostics.lock().unwrap().get("trace-a").cloned();
|
|
assert_eq!(
|
|
diagnostic,
|
|
Some(TestDiagnostic {
|
|
candidate_count: Some(2),
|
|
terminal_reason: Some("no_local_sync_plans"),
|
|
skip_reasons: BTreeMap::from([("transport_missing", 1)]),
|
|
..Default::default()
|
|
})
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn runtime_miss_diagnostic_field_helpers_apply_candidate_reason_state_machine() {
|
|
let mut diagnostic = TestDiagnostic::default();
|
|
|
|
apply_ai_runtime_candidate_evaluation_progress_to_diagnostic(&mut diagnostic, 0);
|
|
assert_eq!(diagnostic.candidate_count, Some(0));
|
|
assert_eq!(diagnostic.reason, "candidate_list_empty");
|
|
|
|
apply_ai_runtime_candidate_evaluation_progress_to_diagnostic(&mut diagnostic, 3);
|
|
assert_eq!(diagnostic.candidate_count, Some(3));
|
|
assert_eq!(diagnostic.reason, "candidate_evaluation_incomplete");
|
|
|
|
record_ai_runtime_candidate_skip_reason_on_diagnostic(
|
|
&mut diagnostic,
|
|
"api_key_concurrency_limit_reached",
|
|
);
|
|
record_ai_runtime_candidate_skip_reason_on_diagnostic(
|
|
&mut diagnostic,
|
|
"api_key_concurrency_limit_reached",
|
|
);
|
|
apply_ai_runtime_candidate_terminal_plan_reason_to_diagnostic(
|
|
&mut diagnostic,
|
|
"no_local_sync_plans",
|
|
);
|
|
assert_eq!(diagnostic.reason, "no_local_sync_plans");
|
|
|
|
diagnostic.candidate_count = Some(2);
|
|
apply_ai_runtime_candidate_terminal_plan_reason_to_diagnostic(
|
|
&mut diagnostic,
|
|
"no_local_sync_plans",
|
|
);
|
|
assert_eq!(diagnostic.reason, "api_key_concurrency_limit_reached");
|
|
|
|
record_ai_runtime_candidate_skip_reason_on_diagnostic(&mut diagnostic, "transport_missing");
|
|
apply_ai_runtime_candidate_terminal_plan_reason_to_diagnostic(
|
|
&mut diagnostic,
|
|
"no_local_sync_plans",
|
|
);
|
|
assert_eq!(diagnostic.reason, "all_candidates_skipped");
|
|
}
|
|
}
|