pub mod client_trait;
pub mod common;
pub mod easy_trait;
pub mod executor;
pub mod protocol_trait;
pub mod summary;
pub mod tools;
pub use client_trait::{
audit_client_action_declarations, client_action_names_for_pattern, client_llm_action_set,
Client,
};
pub use common::{
generate_base_stack_documentation, get_network_event_common_actions,
get_user_input_common_actions,
};
pub use easy_trait::Easy;
pub use protocol_trait::{Protocol, Server};
pub use summary::{summarize_action, summarize_actions};
pub use tools::{
execute_tool, get_all_tool_actions, get_network_event_tool_actions, is_tool_name, ToolAction,
ToolResult, TOOL_ACTION_NAMES,
};
use crate::protocol::log_template::LogTemplate;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Serialize)]
pub struct StartupExamples {
pub llm_mode: serde_json::Value,
pub script_mode: serde_json::Value,
pub static_mode: serde_json::Value,
}
impl StartupExamples {
pub fn new(
llm_mode: serde_json::Value,
script_mode: serde_json::Value,
static_mode: serde_json::Value,
) -> Self {
Self {
llm_mode,
script_mode,
static_mode,
}
}
pub fn validate(&self, protocol_name: &str) -> Result<(), String> {
self.validate_example(&self.llm_mode, "llm_mode", protocol_name)?;
self.validate_example(&self.script_mode, "script_mode", protocol_name)?;
self.validate_example(&self.static_mode, "static_mode", protocol_name)?;
Ok(())
}
fn validate_example(
&self,
example: &serde_json::Value,
mode_name: &str,
protocol_name: &str,
) -> Result<(), String> {
let obj = example.as_object().ok_or_else(|| {
format!(
"Protocol {} {} example must be a JSON object",
protocol_name, mode_name
)
})?;
let action_type = obj.get("type").and_then(|v| v.as_str()).ok_or_else(|| {
format!(
"Protocol {} {} example missing 'type' field",
protocol_name, mode_name
)
})?;
if action_type != "open_server" && action_type != "open_client" {
return Err(format!(
"Protocol {} {} example has type '{}', expected 'open_server' or 'open_client'",
protocol_name, mode_name, action_type
));
}
if !obj.contains_key("base_stack") {
return Err(format!(
"Protocol {} {} example missing 'base_stack' field",
protocol_name, mode_name
));
}
if mode_name == "script_mode" || mode_name == "static_mode" {
let handlers = obj.get("event_handlers").ok_or_else(|| {
format!(
"Protocol {} {} example missing 'event_handlers' array",
protocol_name, mode_name
)
})?;
if !handlers.is_array() {
return Err(format!(
"Protocol {} {} example 'event_handlers' must be an array",
protocol_name, mode_name
));
}
}
if mode_name == "llm_mode" && !obj.contains_key("instruction") {
return Err(format!(
"Protocol {} llm_mode example missing 'instruction' field",
protocol_name
));
}
Ok(())
}
pub fn to_prompt_text(&self) -> String {
let mut text = String::new();
text.push_str("### Starting this Protocol\n\n");
text.push_str("**LLM Mode** (LLM handles all responses intelligently):\n");
text.push_str("```json\n");
text.push_str(&serde_json::to_string_pretty(&self.llm_mode).unwrap_or_default());
text.push_str("\n```\n\n");
text.push_str("**Script Mode** (code-based deterministic responses):\n");
text.push_str("```json\n");
text.push_str(&serde_json::to_string_pretty(&self.script_mode).unwrap_or_default());
text.push_str("\n```\n\n");
text.push_str("**Static Mode** (fixed, unchanging responses):\n");
text.push_str("```json\n");
text.push_str(&serde_json::to_string_pretty(&self.static_mode).unwrap_or_default());
text.push_str("\n```\n\n");
text
}
}
#[derive(Debug, Clone, Serialize)]
pub struct ParameterDefinition {
pub name: String,
pub type_hint: String,
pub description: String,
pub required: bool,
pub example: serde_json::Value,
#[serde(skip_serializing_if = "Option::is_none")]
pub default: Option<serde_json::Value>,
}
impl ParameterDefinition {
pub fn to_prompt_text(&self) -> String {
let required = if self.required {
"required"
} else {
"optional"
};
let mut text = format!(
"\"{}\": {} // {} ({})\nExample: {}",
self.name,
self.type_hint,
self.description,
required,
serde_json::to_string(&self.example).unwrap_or_default()
);
if let Some(default) = &self.default {
text.push_str(&format!(
"\nDefault when omitted: {}",
serde_json::to_string(default).unwrap_or_default()
));
}
text
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ToolCategory {
InformationTool,
Action,
ProtocolAction,
}
#[derive(Debug, Clone)]
pub struct ActionDefinition {
pub name: String,
pub description: String,
pub parameters: Vec<Parameter>,
pub example: serde_json::Value,
pub log_template: Option<LogTemplate>,
}
impl ActionDefinition {
pub fn is_tool(&self) -> bool {
tools::is_tool_name(&self.name)
}
pub fn to_prompt_text(&self) -> String {
let mut text = format!("{}\n\n{}\n", self.name, self.description);
if !self.parameters.is_empty() {
text.push_str("\nParameters:\n");
for param in &self.parameters {
let required = if param.required {
"required"
} else {
"optional"
};
text.push_str(&format!(
"- {}: {} ({}) - {}\n",
param.name, param.type_hint, required, param.description
));
}
}
text.push_str("\nExample:\n```json\n");
text.push_str(&serde_json::to_string_pretty(&self.example).unwrap_or_default());
text.push_str("\n```");
text
}
pub fn with_log_template(mut self, template: LogTemplate) -> Self {
self.log_template = Some(template);
self
}
pub fn category(&self) -> ToolCategory {
if self.is_tool() {
ToolCategory::InformationTool
} else if self.is_common_action() {
ToolCategory::Action
} else {
ToolCategory::ProtocolAction
}
}
pub fn mcp_visible(&self) -> bool {
match self.category() {
ToolCategory::InformationTool => {
!matches!(
self.name.as_str(),
"generate_random" | "list_network_interfaces"
)
}
ToolCategory::Action => {
!matches!(
self.name.as_str(),
"show_message" | "append_to_log" | "provide_feedback"
)
}
ToolCategory::ProtocolAction => {
false
}
}
}
fn is_common_action(&self) -> bool {
matches!(
self.name.as_str(),
"show_message"
| "open_server"
| "close_server"
| "close_all_servers"
| "open_client"
| "close_client"
| "close_all_clients"
| "reconnect_client"
| "update_client_instruction"
| "update_client"
| "update_server"
| "close_connection_by_id"
| "update_instruction"
| "set_memory"
| "append_memory"
| "append_to_log"
| "change_model"
| "schedule_task"
| "cancel_task"
| "provide_feedback"
| "create_database"
| "delete_database"
)
}
pub fn to_tool_schema(&self) -> serde_json::Value {
let mut properties = serde_json::Map::new();
let mut required = Vec::new();
for param in &self.parameters {
let mut prop = serde_json::Map::new();
let json_type = match param.type_hint.as_str() {
"number" | "integer" => "number",
"boolean" | "bool" => "boolean",
"array" => "array",
"object" => "object",
_ => "string",
};
prop.insert(
"type".to_string(),
serde_json::Value::String(json_type.to_string()),
);
if let Some(choices) = param.choices() {
prop.insert(
"enum".to_string(),
serde_json::Value::Array(
choices.into_iter().map(serde_json::Value::String).collect(),
),
);
}
prop.insert(
"description".to_string(),
serde_json::Value::String(param.description.clone()),
);
properties.insert(param.name.clone(), serde_json::Value::Object(prop));
if param.required {
required.push(serde_json::Value::String(param.name.clone()));
}
}
serde_json::json!({
"type": "function",
"function": {
"name": self.name,
"description": self.description,
"parameters": {
"type": "object",
"properties": properties,
"required": required
}
}
})
}
pub fn to_mcp_tool_schema(&self) -> serde_json::Value {
let mut properties = serde_json::Map::new();
let mut required = Vec::new();
for param in &self.parameters {
let mut prop = serde_json::Map::new();
let json_type = match param.type_hint.as_str() {
"number" | "integer" => "number",
"boolean" | "bool" => "boolean",
"array" => "array",
"object" => "object",
_ => "string",
};
prop.insert(
"type".to_string(),
serde_json::Value::String(json_type.to_string()),
);
if let Some(choices) = param.choices() {
prop.insert(
"enum".to_string(),
serde_json::Value::Array(
choices.into_iter().map(serde_json::Value::String).collect(),
),
);
}
prop.insert(
"description".to_string(),
serde_json::Value::String(param.description.clone()),
);
properties.insert(param.name.clone(), serde_json::Value::Object(prop));
if param.required {
required.push(serde_json::Value::String(param.name.clone()));
}
}
serde_json::json!({
"name": self.name,
"description": self.description,
"inputSchema": {
"type": "object",
"properties": properties,
"required": required
}
})
}
}
#[derive(Debug, Clone)]
pub struct Parameter {
pub name: String,
pub type_hint: String,
pub description: String,
pub required: bool,
}
impl Parameter {
pub fn with_choices<I, S>(mut self, choices: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
let rendered: Vec<String> = choices
.into_iter()
.map(|c| format!("\"{}\"", c.into()))
.collect();
if !rendered.is_empty() {
self.type_hint = rendered.join(" | ");
}
self
}
pub fn choices(&self) -> Option<Vec<String>> {
let segments: Vec<&str> = self.type_hint.split('|').map(str::trim).collect();
if segments.len() < 2 {
return None;
}
let mut values = Vec::with_capacity(segments.len());
for segment in segments {
let inner = segment.strip_prefix('"')?.strip_suffix('"')?;
if inner.is_empty() {
return None;
}
values.push(inner.to_string());
}
Some(values)
}
}
fn first_answer_shaped_value(text: &str, start: usize) -> Option<serde_json::Value> {
let mut from = start;
loop {
let candidate = &text[from..];
let value = serde_json::Deserializer::from_str(candidate)
.into_iter::<serde_json::Value>()
.next()
.and_then(|r| r.ok());
match value {
Some(v @ serde_json::Value::Object(_)) => return Some(v),
Some(serde_json::Value::Array(items)) if items.iter().all(|i| i.is_object()) => {
return Some(serde_json::Value::Array(items))
}
_ => {}
}
let next = text[from + 1..]
.find(['{', '['])
.map(|offset| from + 1 + offset)?;
from = next;
}
}
pub fn normalize_action_object(value: &serde_json::Value) -> serde_json::Value {
let obj = match value.as_object() {
Some(o) => o,
None => return value.clone(),
};
let (name, name_was_the_action) = match obj.get("type").and_then(|v| v.as_str()) {
Some(t) => (t.to_string(), false),
None => match obj.get("function").and_then(|v| v.as_str()) {
Some(f) => (f.to_string(), false),
None => match obj.get("name").and_then(|v| v.as_str()) {
Some(n) => (n.to_string(), true),
None => return value.clone(),
},
},
};
let mut out = serde_json::Map::new();
out.insert("type".to_string(), serde_json::Value::String(name));
for (k, v) in obj {
let is_wrapper = matches!(
k.as_str(),
"type" | "function" | "args" | "arguments" | "parameters"
);
if is_wrapper || (k == "name" && name_was_the_action) {
continue;
}
out.insert(k.clone(), v.clone());
}
for wrapper in ["args", "arguments", "parameters"] {
if let Some(serde_json::Value::Object(inner)) = obj.get(wrapper) {
for (k, v) in inner {
out.entry(k.clone()).or_insert_with(|| v.clone());
}
}
}
serde_json::Value::Object(out)
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ActionResponse {
#[serde(default)]
pub actions: Vec<serde_json::Value>,
#[serde(default)]
pub tools: Vec<serde_json::Value>,
}
impl ActionResponse {
#[allow(clippy::should_implement_trait)]
pub fn from_str(s: &str) -> anyhow::Result<Self> {
let trimmed = s.trim();
let json_str = if trimmed.starts_with("```") {
let start = trimmed.find('\n').unwrap_or(3);
let end = trimmed[start..]
.rfind("```")
.map(|pos| start + pos)
.unwrap_or(trimmed.len());
if start <= end {
trimmed[start..end].trim()
} else {
trimmed
}
} else {
trimmed
};
let json_start = match (json_str.find('{'), json_str.find('[')) {
(Some(b), Some(k)) => b.min(k),
(Some(b), None) => b,
(None, Some(k)) => k,
(None, None) => 0,
};
let clean_json = &json_str[json_start..];
let first_value = first_answer_shaped_value(json_str, json_start);
if let Some(json_value) = first_value {
use crate::llm::actions::tools::ToolAction;
match json_value {
serde_json::Value::Object(ref map)
if map.contains_key("tools") || map.contains_key("actions") =>
{
match serde_json::from_value::<ActionResponse>(json_value.clone()) {
Ok(mut response) => {
for a in response.actions.iter_mut() {
*a = normalize_action_object(a);
}
for t in response.tools.iter_mut() {
*t = normalize_action_object(t);
}
Self::separate_tools_and_actions(&mut response);
return Ok(response);
}
Err(e) => {
anyhow::bail!(
"Failed to parse action response\n\n❌ Expected format:\n{{\n \"tools\": [...],\n \"actions\": [...]\n}}\n\n❌ Actual response:\n{}\n\nError: {}",
clean_json,
e
)
}
}
}
serde_json::Value::Object(ref map)
if map.contains_key("tool_calls") || map.contains_key("tool_call") =>
{
let mut response = ActionResponse::empty();
let calls = map.get("tool_calls").or_else(|| map.get("tool_call"));
let items: Vec<serde_json::Value> = match calls {
Some(serde_json::Value::Array(a)) => a.clone(),
Some(v @ serde_json::Value::Object(_)) => vec![v.clone()],
_ => Vec::new(),
};
for item in &items {
let norm = normalize_action_object(item);
if ToolAction::is_tool_action(&norm) {
response.tools.push(norm);
} else {
response.actions.push(norm);
}
}
return Ok(response);
}
serde_json::Value::Object(ref map)
if map.contains_key("type")
|| map.contains_key("function")
|| map.contains_key("name") =>
{
let norm = normalize_action_object(&json_value);
let mut response = ActionResponse::empty();
if ToolAction::is_tool_action(&norm) {
response.tools.push(norm);
} else {
response.actions.push(norm);
}
return Ok(response);
}
serde_json::Value::Array(items) => {
let mut response = ActionResponse::empty();
for item in &items {
let norm = normalize_action_object(item);
if ToolAction::is_tool_action(&norm) {
response.tools.push(norm);
} else {
response.actions.push(norm);
}
}
return Ok(response);
}
serde_json::Value::Object(ref map) if map.is_empty() => {
return Ok(ActionResponse::empty());
}
_ => {
anyhow::bail!(
"Failed to parse action response: Unrecognized format\n\n❌ Expected one of:\n\
1. {{\n \"tools\": [...],\n \"actions\": [...]\n }}\n\
2. {{\"type\": \"...\", ...}}\n\
3. [{{\"type\": \"...\"}}, ...]\n\n❌ Actual response:\n{}",
clean_json
)
}
}
} else {
anyhow::bail!(
"Failed to parse action response: Invalid JSON\n\n❌ Actual response:\n{}",
clean_json
)
}
}
fn separate_tools_and_actions(response: &mut ActionResponse) {
use crate::llm::actions::tools::ToolAction;
if !response.tools.is_empty() {
let (actual_tools, misplaced_actions): (Vec<_>, Vec<_>) = response
.tools
.clone()
.into_iter()
.partition(|item| ToolAction::is_tool_action(item));
response.tools = actual_tools;
response.actions.extend(misplaced_actions);
}
if !response.actions.is_empty() {
let (misplaced_tools, actual_actions): (Vec<_>, Vec<_>) = response
.actions
.clone()
.into_iter()
.partition(|item| ToolAction::is_tool_action(item));
response.actions = actual_actions;
response.tools.extend(misplaced_tools);
}
}
pub fn empty() -> Self {
Self {
actions: Vec::new(),
tools: Vec::new(),
}
}
pub fn all_actions(&self) -> Vec<serde_json::Value> {
let mut all = self.tools.clone();
all.extend(self.actions.clone());
all
}
}
impl std::str::FromStr for ActionResponse {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
ActionResponse::from_str(s)
}
}