use super::protocol_trait::Protocol;
use super::ActionDefinition;
use crate::state::app_state::AppState;
use anyhow::Result;
#[derive(Debug)]
pub enum ClientActionResult {
SendData(Vec<u8>),
Disconnect,
WaitForMore,
NoAction,
Multiple(Vec<ClientActionResult>),
Custom {
name: String,
data: serde_json::Value,
},
}
impl ClientActionResult {
pub fn has_data(&self) -> bool {
match self {
ClientActionResult::SendData(_) => true,
ClientActionResult::Multiple(results) => results.iter().any(|r| r.has_data()),
_ => false,
}
}
pub fn disconnects(&self) -> bool {
match self {
ClientActionResult::Disconnect => true,
ClientActionResult::Multiple(results) => results.iter().any(|r| r.disconnects()),
_ => false,
}
}
pub fn waits_for_more(&self) -> bool {
match self {
ClientActionResult::WaitForMore => true,
ClientActionResult::Multiple(results) => results.iter().any(|r| r.waits_for_more()),
_ => false,
}
}
pub fn get_all_data(&self) -> Vec<Vec<u8>> {
match self {
ClientActionResult::SendData(data) => vec![data.clone()],
ClientActionResult::Multiple(results) => {
results.iter().flat_map(|r| r.get_all_data()).collect()
}
_ => Vec::new(),
}
}
}
pub trait Client: Protocol {
fn connect(
&self,
ctx: crate::protocol::ConnectContext,
) -> std::pin::Pin<
Box<dyn std::future::Future<Output = anyhow::Result<std::net::SocketAddr>> + Send>,
>;
fn execute_action(&self, action: serde_json::Value) -> Result<ClientActionResult>;
}
pub fn client_llm_action_set(
protocol: &dyn Client,
state: &AppState,
event: Option<&crate::protocol::Event>,
) -> Vec<ActionDefinition> {
let mut seen: std::collections::HashSet<String> = std::collections::HashSet::new();
let mut out: Vec<ActionDefinition> = Vec::new();
let event_actions = event
.map(|e| e.event_type.actions.clone())
.unwrap_or_default();
for action in protocol
.get_async_actions(state)
.into_iter()
.chain(protocol.get_sync_actions())
.chain(event_actions)
{
if seen.insert(action.name.clone()) {
out.push(action);
}
}
out
}
pub fn client_action_names_for_pattern(
protocol: &dyn Client,
state: &AppState,
pattern: &crate::scripting::EventPattern,
) -> Option<Vec<String>> {
let event_types = protocol.get_event_types();
let matching: Vec<_> = event_types
.iter()
.filter(|et| pattern.matches(&et.id))
.collect();
if matching.is_empty() {
return None;
}
let mut names: Vec<String> = client_llm_action_set(protocol, state, None)
.into_iter()
.map(|a| a.name)
.collect();
for event_type in matching {
names.extend(event_type.actions.iter().map(|a| a.name.clone()));
}
Some(names)
}
pub fn audit_client_action_declarations(protocol: &dyn Client, state: &AppState) -> Vec<String> {
let mut findings = Vec::new();
let visible = client_llm_action_set(protocol, state, None);
let visible_names: std::collections::HashSet<&str> =
visible.iter().map(|a| a.name.as_str()).collect();
if visible.is_empty() {
findings.push(format!(
"client '{}' advertises no actions at all: get_async_actions(), get_sync_actions() \
and every event type are empty, so the model is handed nothing protocol-specific \
and anything it returns is rejected as an unknown action. The client cannot be \
driven.",
protocol.protocol_name()
));
}
let mut missing_sync: Vec<String> = protocol
.get_sync_actions()
.into_iter()
.filter(|a| !visible_names.contains(a.name.as_str()))
.map(|a| a.name)
.collect();
missing_sync.sort();
missing_sync.dedup();
if !missing_sync.is_empty() {
findings.push(format!(
"client '{}' declares {} sync action(s) the model is never shown: {}. \
call_llm_for_client must advertise the union of async, sync and the event's own \
actions (see client_llm_action_set); anything it omits is rejected at runtime as \
an unknown action.",
protocol.protocol_name(),
missing_sync.len(),
missing_sync.join(", ")
));
}
for event_type in protocol.get_event_types() {
let mut missing_event: Vec<String> = event_type
.actions
.iter()
.filter(|a| !visible_names.contains(a.name.as_str()))
.map(|a| a.name.clone())
.collect();
missing_event.sort();
missing_event.dedup();
if !missing_event.is_empty() {
findings.push(format!(
"client '{}' attaches action(s) to event '{}' that the model is never shown: \
{}. Either add them to get_async_actions()/get_sync_actions(), or fix \
call_llm_for_client to advertise the event's own list.",
protocol.protocol_name(),
event_type.id,
missing_event.join(", ")
));
}
}
findings
}