use crate::llm::actions::client_trait::client_llm_action_set;
use crate::llm::actions::ActionDefinition;
use crate::state::app_state::AppState;
use crate::state::client_handles::ClientSendOutcome;
use crate::state::intercepts::{InterceptOwner, InterceptView};
fn union_into(out: &mut Vec<ActionDefinition>, extra: Vec<ActionDefinition>) {
for action in extra {
if !out.iter().any(|a| a.name == action.name) {
out.push(action);
}
}
}
pub fn client_vocabulary(
state: &AppState,
protocol_name: &str,
) -> Result<Vec<ActionDefinition>, String> {
let protocol = crate::cli::client_startup::resolve_client_protocol(protocol_name)
.and_then(|name| crate::protocol::CLIENT_REGISTRY.get(&name))
.ok_or_else(|| format!("client protocol '{protocol_name}' is not compiled in"))?;
Ok(client_llm_action_set(protocol.as_ref(), state, None))
}
pub fn peer_vocabulary(
state: &AppState,
protocol_name: &str,
) -> Result<Vec<ActionDefinition>, String> {
let protocol = crate::protocol::server_registry::registry()
.resolve(protocol_name)
.map_err(|e| e.to_string())?;
let mut actions = protocol.get_sync_actions();
union_into(&mut actions, protocol.get_async_actions(state));
Ok(actions)
}
pub async fn intercept_vocabulary(
state: &AppState,
view: &InterceptView,
) -> Result<(String, Vec<ActionDefinition>), String> {
match view.owner {
InterceptOwner::Server(server_id) => {
let server = state
.get_server(server_id)
.await
.ok_or_else(|| format!("server #{} no longer exists", server_id.as_u32()))?;
let protocol = crate::protocol::server_registry::registry()
.resolve(&server.protocol_name)
.map_err(|e| e.to_string())?;
let mut actions: Vec<ActionDefinition> = protocol
.get_event_types()
.into_iter()
.find(|e| e.id == view.event_type)
.map(|e| e.actions)
.unwrap_or_default();
union_into(&mut actions, protocol.get_sync_actions());
Ok((server.protocol_name, actions))
}
InterceptOwner::Client(client_id) => {
let client = state
.get_client(client_id)
.await
.ok_or_else(|| format!("client #{} no longer exists", client_id.as_u32()))?;
let protocol =
crate::cli::client_startup::resolve_client_protocol(&client.protocol_name)
.and_then(|name| crate::protocol::CLIENT_REGISTRY.get(&name))
.ok_or_else(|| {
format!(
"client protocol '{}' is not compiled in",
client.protocol_name
)
})?;
let mut actions = client_llm_action_set(protocol.as_ref(), state, None);
let event_actions = protocol
.get_event_types()
.into_iter()
.find(|e| e.id == view.event_type)
.map(|e| e.actions)
.unwrap_or_default();
union_into(&mut actions, event_actions);
Ok((client.protocol_name, actions))
}
}
}
pub fn check_action_types(
actions: &[serde_json::Value],
allowed: &[ActionDefinition],
also_allowed: &[&str],
) -> Result<(), String> {
for (index, action) in actions.iter().enumerate() {
let Some(object) = action.as_object() else {
return Err(format!(
"action {index} is not a JSON object: {action}. Each action is \
{{\"type\": \"<name>\", ...parameters}}."
));
};
let Some(name) = object.get("type").and_then(|t| t.as_str()) else {
return Err(format!(
"action {index} has no string \"type\" field naming the action: {action}"
));
};
if allowed.iter().any(|a| a.name == name) || also_allowed.contains(&name) {
continue;
}
let mut names: Vec<&str> = allowed.iter().map(|a| a.name.as_str()).collect();
names.extend(also_allowed.iter().copied());
return Err(format!(
"'{name}' is not an action this target accepts. Accepted: {}",
if names.is_empty() {
"(none — the protocol declares no actions here)".to_string()
} else {
names.join(", ")
}
));
}
Ok(())
}
pub fn action_signature(action: &ActionDefinition) -> String {
let params: Vec<String> = action
.parameters
.iter()
.map(|p| {
if p.required {
format!("{}*", p.name)
} else {
p.name.clone()
}
})
.collect();
format!("{}({})", action.name, params.join(", "))
}
pub fn describe_outcome(outcome: &ClientSendOutcome) -> String {
match outcome {
ClientSendOutcome::Sent { bytes_sent } => {
format!("sent {bytes_sent} byte(s) on the wire")
}
ClientSendOutcome::Executed { detail } => {
format!("executed, nothing written to the wire ({detail})")
}
ClientSendOutcome::Rejected { error } => format!("rejected by the protocol: {error}"),
ClientSendOutcome::Disconnected => {
"the connection is closed (the action asked to disconnect, or the loop had already \
exited)"
.to_string()
}
}
}