use anyhow::Result;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use tokio::sync::mpsc;
use crate::llm::actions::common::ServerTaskDefinition;
use crate::llm::actions::ParameterDefinition;
use crate::llm::OllamaClient;
use crate::state::app_state::AppState;
use crate::state::{ClientId, ServerId};
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ServerForm {
pub protocol: String,
pub port: Option<u16>,
pub host: Option<String>,
pub interface: Option<String>,
pub mac_address: Option<String>,
#[serde(default)]
pub send_first: bool,
pub instruction: Option<String>,
pub initial_memory: Option<String>,
pub startup_params: Option<Value>,
pub event_handlers: Option<Vec<Value>>,
pub scheduled_tasks: Option<Vec<ServerTaskDefinition>>,
pub feedback_instructions: Option<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ClientForm {
pub protocol: String,
pub remote_addr: Option<String>,
pub instruction: Option<String>,
pub initial_memory: Option<String>,
pub startup_params: Option<Value>,
pub event_handlers: Option<Vec<Value>>,
pub scheduled_tasks: Option<Vec<ServerTaskDefinition>>,
pub feedback_instructions: Option<String>,
}
#[derive(Debug, Clone)]
pub struct UpdateOutcome {
pub id: u32,
pub restarted: bool,
pub summary: String,
}
pub fn server_declared_params(protocol: &str) -> Option<Vec<ParameterDefinition>> {
crate::protocol::server_registry::registry()
.resolve(protocol)
.ok()
.map(|p| p.get_startup_parameters())
}
pub fn client_declared_params(protocol: &str) -> Option<Vec<ParameterDefinition>> {
crate::protocol::CLIENT_REGISTRY
.resolve(protocol)
.ok()
.map(|p| p.get_startup_parameters())
}
fn validate_params_declared(
params: &Value,
schema: &[ParameterDefinition],
protocol: &str,
) -> Result<()> {
crate::protocol::StartupParams::preflight(params)?;
crate::protocol::spawn_context::StartupParams::new_validated(params.clone(), schema.to_vec())
.map(|_| ())
.map_err(|e| anyhow::anyhow!("Invalid startup_params for {}: {}", protocol, e))
}
pub(crate) fn validate_server_startup_params(
params: &Value,
schema: &[ParameterDefinition],
protocol: &str,
) -> Result<()> {
validate_params_declared(params, schema, protocol)?;
{
let names: std::collections::HashSet<_> = schema.iter().map(|p| p.name.as_str()).collect();
if [
"cert_path",
"key_path",
"common_name",
"san_dns_names",
"validity_days",
"organization",
"organizational_unit",
]
.iter()
.all(|name| names.contains(name))
{
let validated =
crate::protocol::StartupParams::new_validated(params.clone(), schema.to_vec())?;
if names.contains("tls_enabled") {
crate::server::tls_cert_manager::extract_tls_config_from_params(&validated)?;
} else {
crate::server::tls_cert_manager::extract_required_tls_config_from_params(
&validated,
)?;
}
}
}
Ok(())
}
impl ServerForm {
pub async fn create(
mut self,
state: &AppState,
status_tx: mpsc::UnboundedSender<String>,
) -> Result<ServerId> {
crate::protocol::StartupParams::preflight_owned(&mut self.startup_params)?;
let instruction = self.instruction.clone().unwrap_or_else(|| {
format!(
"You are a {} server. Handle requests appropriately.",
self.protocol
)
});
crate::cli::server_startup::start_server_from_action(
state,
self.mac_address,
self.interface,
self.host,
self.port,
&self.protocol,
self.send_first,
self.initial_memory,
instruction,
self.startup_params,
self.event_handlers,
self.scheduled_tasks,
self.feedback_instructions,
status_tx,
)
.await
}
}
impl ClientForm {
pub async fn create(
mut self,
state: &AppState,
llm_client: OllamaClient,
status_tx: mpsc::UnboundedSender<String>,
) -> Result<ClientId> {
crate::protocol::StartupParams::preflight_owned(&mut self.startup_params)?;
let remote_addr = self
.remote_addr
.clone()
.ok_or_else(|| anyhow::anyhow!("remote_addr is required to open a client"))?;
let instruction = self.instruction.clone().unwrap_or_else(|| {
format!(
"You are a {} client connected to {}. Handle responses appropriately.",
self.protocol, remote_addr
)
});
crate::cli::client_startup::start_client_from_action(
state,
&self.protocol,
&remote_addr,
instruction,
self.startup_params,
self.initial_memory,
self.event_handlers,
self.scheduled_tasks,
self.feedback_instructions,
llm_client,
Some(status_tx),
)
.await
}
}
fn merge_params(base: Option<&Value>, overlay: &Value) -> Result<Value> {
if !crate::utils::json_budget::within_values_budget(
base.into_iter().chain(std::iter::once(overlay)),
crate::protocol::spawn_context::MAX_STARTUP_PARAM_BYTES,
crate::protocol::spawn_context::MAX_STARTUP_PARAM_NODES,
crate::protocol::spawn_context::MAX_STARTUP_PARAM_DEPTH,
) {
return Err(anyhow::anyhow!(
"startup_params exceeds the merged JSON size, node or depth budget"
));
}
let map = overlay
.as_object()
.ok_or_else(|| anyhow::anyhow!("startup_params must be a JSON object"))?;
let mut out = base
.and_then(|v| v.as_object().cloned())
.unwrap_or_default();
for (k, v) in map {
out.insert(k.clone(), v.clone());
}
Ok(Value::Object(out))
}
fn server_needs_restart(form: &ServerForm) -> bool {
form.port.is_some()
|| form.host.is_some()
|| form.interface.is_some()
|| form.mac_address.is_some()
|| form.startup_params.is_some()
}
pub async fn update_server(
state: &AppState,
server_id: ServerId,
mut form: ServerForm,
status_tx: mpsc::UnboundedSender<String>,
) -> Result<UpdateOutcome> {
crate::protocol::StartupParams::preflight_owned(&mut form.startup_params)?;
let current = state
.with_server_mut(server_id, |current| {
crate::protocol::StartupParams::preflight_owned(&mut current.startup_params)?;
Ok::<_, crate::protocol::StartupParamError>(current.clone())
})
.await
.ok_or_else(|| anyhow::anyhow!("Server #{} not found", server_id.as_u32()))??;
if !form.protocol.is_empty() && !form.protocol.eq_ignore_ascii_case(¤t.protocol_name) {
return Err(anyhow::anyhow!(
"Cannot change protocol of server #{} from '{}' to '{}'. Stop it and open a new one instead.",
server_id.as_u32(),
current.protocol_name,
form.protocol
));
}
let protocol_impl = crate::protocol::server_registry::registry()
.resolve(¤t.protocol_name)
.map_err(|_| {
anyhow::anyhow!("Server protocol '{}' not available", current.protocol_name)
})?;
let prepared_tasks = crate::state::task::prepare_tasks(
form.scheduled_tasks.as_deref(),
crate::state::task::TaskScope::Server(server_id),
)?;
let merged_params: Option<Value> = match &form.startup_params {
Some(overlay) => Some(merge_params(current.startup_params.as_ref(), overlay)?),
None => current.startup_params.clone(),
};
if let Some(params) = &merged_params {
let schema = protocol_impl.get_startup_parameters();
validate_server_startup_params(params, &schema, ¤t.protocol_name)?;
}
if let Some(handlers) = &form.event_handlers {
crate::events::handler::EventHandler::parse_event_handlers(handlers.clone())
.map_err(|e| anyhow::anyhow!("Invalid event_handlers: {}", e))?;
}
if server_needs_restart(&form) {
let mut changed = Vec::new();
if form.port.is_some() {
changed.push("port");
}
if form.host.is_some() {
changed.push("host");
}
if form.interface.is_some() {
changed.push("interface");
}
if form.mac_address.is_some() {
changed.push("mac_address");
}
if form.startup_params.is_some() {
changed.push("startup_params");
}
let instruction = form
.instruction
.clone()
.unwrap_or(current.instruction.clone());
let initial_memory = match &form.initial_memory {
Some(m) => Some(m.clone()),
None if !current.memory.is_empty() => Some(current.memory.clone()),
None => None,
};
let event_handlers: Option<Vec<Value>> = match &form.event_handlers {
Some(h) => Some(h.clone()),
None => current.event_handler_config.as_ref().and_then(|cfg| {
serde_json::to_value(&cfg.handlers)
.ok()
.and_then(|v| v.as_array().cloned())
}),
};
let feedback_instructions = form
.feedback_instructions
.clone()
.or(current.feedback_instructions.clone());
let port = form.port.or(Some(current.port));
state.remove_server(server_id).await;
let _ = status_tx.send(format!(
"[SERVER] Restarting server #{} ({}) to apply {}",
server_id.as_u32(),
current.protocol_name,
changed.join(", ")
));
let new_id = crate::cli::server_startup::start_server_from_action(
state,
form.mac_address,
form.interface,
form.host,
port,
¤t.protocol_name,
form.send_first,
initial_memory,
instruction,
merged_params,
event_handlers,
form.scheduled_tasks,
feedback_instructions,
status_tx.clone(),
)
.await?;
let summary = format!(
"Server #{} restarted as #{} (changed: {}); live connections were dropped.",
server_id.as_u32(),
new_id.as_u32(),
changed.join(", ")
);
let _ = status_tx.send(format!("[SERVER] {}", summary));
let _ = status_tx.send("__UPDATE_UI__".to_string());
return Ok(UpdateOutcome {
id: new_id.as_u32(),
restarted: true,
summary,
});
}
let mut changed = Vec::new();
if let Some(instruction) = &form.instruction {
state.set_instruction(server_id, instruction.clone()).await;
changed.push("instruction");
}
if let Some(memory) = &form.initial_memory {
state.set_memory(server_id, memory.clone()).await;
changed.push("memory");
}
if let Some(handlers) = &form.event_handlers {
let config = crate::events::handler::EventHandler::parse_event_handlers(handlers.clone())
.map_err(|e| anyhow::anyhow!("Invalid event_handlers: {}", e))?;
state
.with_server_mut(server_id, |s| {
s.event_handler_config = Some(config);
})
.await;
changed.push("event_handlers");
}
if let Some(fb) = &form.feedback_instructions {
state
.with_server_mut(server_id, |s| {
s.feedback_instructions = Some(fb.clone());
})
.await;
changed.push("feedback_instructions");
}
if let Some(tasks) = form.scheduled_tasks {
let n = tasks.len();
for task in prepared_tasks {
state.add_task(task).await;
}
if n > 0 {
changed.push("scheduled_tasks");
}
}
let summary = if changed.is_empty() {
format!("Server #{}: nothing to update.", server_id.as_u32())
} else {
format!(
"Server #{} updated in place ({}); connections preserved.",
server_id.as_u32(),
changed.join(", ")
)
};
let _ = status_tx.send(format!("[SERVER] {}", summary));
let _ = status_tx.send("__UPDATE_UI__".to_string());
Ok(UpdateOutcome {
id: server_id.as_u32(),
restarted: false,
summary,
})
}
fn client_needs_restart(form: &ClientForm) -> bool {
form.remote_addr.is_some() || form.startup_params.is_some()
}
pub async fn update_client(
state: &AppState,
client_id: ClientId,
mut form: ClientForm,
llm_client: OllamaClient,
status_tx: mpsc::UnboundedSender<String>,
) -> Result<UpdateOutcome> {
crate::protocol::StartupParams::preflight_owned(&mut form.startup_params)?;
let current = state
.with_client_mut(client_id, |current| {
crate::protocol::StartupParams::preflight_owned(&mut current.startup_params)?;
Ok::<_, crate::protocol::StartupParamError>(current.clone())
})
.await
.ok_or_else(|| anyhow::anyhow!("Client #{} not found", client_id.as_u32()))??;
if !form.protocol.is_empty() && !form.protocol.eq_ignore_ascii_case(¤t.protocol_name) {
return Err(anyhow::anyhow!(
"Cannot change protocol of client #{} from '{}' to '{}'. Stop it and open a new one instead.",
client_id.as_u32(),
current.protocol_name,
form.protocol
));
}
let protocol_impl = crate::protocol::CLIENT_REGISTRY
.resolve(¤t.protocol_name)
.map_err(|_| {
anyhow::anyhow!("Client protocol '{}' not available", current.protocol_name)
})?;
let prepared_tasks = crate::state::task::prepare_tasks(
form.scheduled_tasks.as_deref(),
crate::state::task::TaskScope::Client(client_id),
)?;
let merged_params: Option<Value> = match &form.startup_params {
Some(overlay) => Some(merge_params(current.startup_params.as_ref(), overlay)?),
None => current.startup_params.clone(),
};
if let Some(params) = &merged_params {
let schema = protocol_impl.get_startup_parameters();
validate_params_declared(params, &schema, ¤t.protocol_name)?;
}
if let Some(handlers) = &form.event_handlers {
validate_client_handlers(handlers)?;
}
if client_needs_restart(&form) {
let mut changed = Vec::new();
if form.remote_addr.is_some() {
changed.push("remote_addr");
}
if form.startup_params.is_some() {
changed.push("startup_params");
}
let remote_addr = form
.remote_addr
.clone()
.unwrap_or(current.remote_addr.clone());
let instruction = form
.instruction
.clone()
.unwrap_or(current.instruction.clone());
let initial_memory = match &form.initial_memory {
Some(m) => Some(m.clone()),
None if !current.memory.is_empty() => Some(current.memory.clone()),
None => None,
};
let event_handlers: Option<Vec<Value>> = match &form.event_handlers {
Some(h) => Some(h.clone()),
None => current.event_handler_config.as_ref().and_then(|cfg| {
serde_json::to_value(&cfg.handlers)
.ok()
.and_then(|v| v.as_array().cloned())
}),
};
let feedback_instructions = form
.feedback_instructions
.clone()
.or(current.feedback_instructions.clone());
state.remove_client(client_id).await;
let _ = status_tx.send(format!(
"[CLIENT] Reconnecting client #{} ({}) to apply {}",
client_id.as_u32(),
current.protocol_name,
changed.join(", ")
));
let new_id = crate::cli::client_startup::start_client_from_action(
state,
¤t.protocol_name,
&remote_addr,
instruction,
merged_params,
initial_memory,
event_handlers,
form.scheduled_tasks,
feedback_instructions,
llm_client,
Some(status_tx.clone()),
)
.await?;
let summary = format!(
"Client #{} reconnected as #{} (changed: {}).",
client_id.as_u32(),
new_id.as_u32(),
changed.join(", ")
);
let _ = status_tx.send(format!("[CLIENT] {}", summary));
let _ = status_tx.send("__UPDATE_UI__".to_string());
return Ok(UpdateOutcome {
id: new_id.as_u32(),
restarted: true,
summary,
});
}
let mut changed = Vec::new();
if let Some(instruction) = &form.instruction {
state
.set_instruction_for_client(client_id, instruction.clone())
.await;
changed.push("instruction");
}
if let Some(memory) = &form.initial_memory {
state.set_memory_for_client(client_id, memory.clone()).await;
changed.push("memory");
}
if let Some(handlers) = &form.event_handlers {
let config = parse_client_handlers(handlers)?;
state
.with_client_mut(client_id, |c| {
c.event_handler_config = Some(config);
})
.await;
changed.push("event_handlers");
}
if let Some(fb) = &form.feedback_instructions {
state
.with_client_mut(client_id, |c| {
c.feedback_instructions = Some(fb.clone());
})
.await;
changed.push("feedback_instructions");
}
if let Some(tasks) = form.scheduled_tasks {
let n = tasks.len();
for task in prepared_tasks {
state.add_task(task).await;
}
if n > 0 {
changed.push("scheduled_tasks");
}
}
let summary = if changed.is_empty() {
format!("Client #{}: nothing to update.", client_id.as_u32())
} else {
format!(
"Client #{} updated in place ({}).",
client_id.as_u32(),
changed.join(", ")
)
};
let _ = status_tx.send(format!("[CLIENT] {}", summary));
let _ = status_tx.send("__UPDATE_UI__".to_string());
Ok(UpdateOutcome {
id: client_id.as_u32(),
restarted: false,
summary,
})
}
fn validate_client_handlers(handlers: &[Value]) -> Result<()> {
parse_client_handlers(handlers).map(|_| ())
}
fn parse_client_handlers(handlers: &[Value]) -> Result<crate::scripting::EventHandlerConfig> {
use crate::scripting::{EventHandler, EventHandlerConfig};
let mut parsed = Vec::with_capacity(handlers.len());
for (i, h) in handlers.iter().enumerate() {
let handler: EventHandler = serde_json::from_value(h.clone())
.map_err(|e| anyhow::anyhow!("Invalid event_handlers[{}]: {}", i, e))?;
parsed.push(handler);
}
Ok(EventHandlerConfig { handlers: parsed })
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum FormTarget {
CreateServer,
CreateClient,
UpdateServer(u32),
UpdateClient(u32),
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum FieldTarget {
Port,
RemoteAddr,
Instruction,
Memory,
EventHandlers,
FeedbackInstructions,
StartupParam {
type_hint: String,
},
}
#[derive(Debug, Clone)]
pub struct FormField {
pub name: String,
pub type_label: String,
pub required: bool,
pub help: String,
pub prefill: String,
pub value: Option<String>,
target: FieldTarget,
}
#[derive(Debug, Clone)]
pub struct InteractiveForm {
pub target: FormTarget,
pub protocol: String,
pub fields: Vec<FormField>,
pub index: usize,
}
#[derive(Debug, Clone, Default)]
pub struct ServerPrefill {
pub instruction: String,
pub memory: String,
pub port: u16,
pub startup_params: Option<Value>,
pub event_handlers: Option<Vec<Value>>,
pub feedback_instructions: Option<String>,
}
impl ServerPrefill {
pub fn from_server(server: &crate::state::server::ServerInstance) -> Self {
Self {
instruction: server.instruction.clone(),
memory: server.memory.clone(),
port: server
.local_addr
.map(|a| a.port())
.filter(|p| *p != 0)
.unwrap_or(server.port),
startup_params: server.startup_params.clone(),
event_handlers: server.event_handler_config.as_ref().and_then(|c| {
serde_json::to_value(&c.handlers)
.ok()
.and_then(|v| v.as_array().cloned())
}),
feedback_instructions: server.feedback_instructions.clone(),
}
}
}
#[derive(Debug, Clone, Default)]
pub struct ClientPrefill {
pub remote_addr: String,
pub instruction: String,
pub memory: String,
pub startup_params: Option<Value>,
pub event_handlers: Option<Vec<Value>>,
pub feedback_instructions: Option<String>,
}
fn param_value_to_prefill(v: &Value) -> String {
match v {
Value::String(s) => s.clone(),
other => serde_json::to_string(other).unwrap_or_default(),
}
}
fn startup_param_fields(schema: &[ParameterDefinition], current: Option<&Value>) -> Vec<FormField> {
let current_obj = current.and_then(|v| v.as_object());
schema
.iter()
.map(|p| {
let prefill = current_obj
.and_then(|o| o.get(&p.name))
.map(param_value_to_prefill)
.unwrap_or_default();
FormField {
name: p.name.clone(),
type_label: p.type_hint.clone(),
required: p.required,
help: p.description.clone(),
prefill,
value: None,
target: FieldTarget::StartupParam {
type_hint: p.type_hint.clone(),
},
}
})
.collect()
}
fn field(name: &str, ty: &str, help: &str, prefill: String, target: FieldTarget) -> FormField {
FormField {
name: name.to_string(),
type_label: ty.to_string(),
required: false,
help: help.to_string(),
prefill,
value: None,
target,
}
}
fn handlers_to_prefill(handlers: &Option<Vec<Value>>) -> String {
match handlers {
Some(h) if !h.is_empty() => serde_json::to_string(h).unwrap_or_default(),
_ => String::new(),
}
}
impl InteractiveForm {
pub fn create_server(protocol: &str, schema: &[ParameterDefinition]) -> Self {
let mut fields = common_server_fields(&ServerPrefill::default(), false);
fields.extend(startup_param_fields(schema, None));
Self {
target: FormTarget::CreateServer,
protocol: protocol.to_string(),
fields,
index: 0,
}
}
pub fn create_client(protocol: &str, schema: &[ParameterDefinition]) -> Self {
let mut fields = common_client_fields(&ClientPrefill::default(), false);
fields.extend(startup_param_fields(schema, None));
Self {
target: FormTarget::CreateClient,
protocol: protocol.to_string(),
fields,
index: 0,
}
}
pub fn update_server(
id: u32,
protocol: &str,
schema: &[ParameterDefinition],
current: &ServerPrefill,
) -> Self {
let mut fields = common_server_fields(current, true);
fields.extend(startup_param_fields(
schema,
current.startup_params.as_ref(),
));
Self {
target: FormTarget::UpdateServer(id),
protocol: protocol.to_string(),
fields,
index: 0,
}
}
pub fn update_client(
id: u32,
protocol: &str,
schema: &[ParameterDefinition],
current: &ClientPrefill,
) -> Self {
let mut fields = common_client_fields(current, true);
fields.extend(startup_param_fields(
schema,
current.startup_params.as_ref(),
));
Self {
target: FormTarget::UpdateClient(id),
protocol: protocol.to_string(),
fields,
index: 0,
}
}
pub fn current_field(&self) -> Option<&FormField> {
self.fields.get(self.index)
}
pub fn prompt(&self) -> String {
match self.current_field() {
None => "Form complete.".to_string(),
Some(f) => {
let req = if f.required { ", required" } else { "" };
let hint = if f.required {
" (required)"
} else if matches!(
self.target,
FormTarget::CreateServer | FormTarget::CreateClient
) {
" (empty = protocol default)"
} else {
" (empty = keep current)"
};
format!(
"Field {}/{}: {} ({}{}) — {}{}",
self.index + 1,
self.fields.len(),
f.name,
f.type_label,
req,
f.help,
hint,
)
}
}
}
pub fn title(&self) -> String {
match self.target {
FormTarget::CreateServer => format!("Create {} server", self.protocol),
FormTarget::CreateClient => format!("Create {} client", self.protocol),
FormTarget::UpdateServer(id) => {
format!("Update {} server #{}", self.protocol, id)
}
FormTarget::UpdateClient(id) => {
format!("Update {} client #{}", self.protocol, id)
}
}
}
pub fn current_prefill(&self) -> String {
self.current_field()
.map(|f| f.prefill.clone())
.unwrap_or_default()
}
pub fn submit_current(&mut self, raw: &str) {
if let Some(f) = self.fields.get_mut(self.index) {
f.value = Some(raw.trim().to_string());
}
self.index += 1;
}
pub fn set_field_value(&mut self, name: &str, value: &str) -> bool {
if let Some(f) = self.fields.iter_mut().find(|f| f.name == name) {
f.value = Some(value.trim().to_string());
true
} else {
false
}
}
pub fn is_complete(&self) -> bool {
self.index >= self.fields.len()
}
fn is_client(&self) -> bool {
matches!(
self.target,
FormTarget::CreateClient | FormTarget::UpdateClient(_)
)
}
fn is_update(&self) -> bool {
matches!(
self.target,
FormTarget::UpdateServer(_) | FormTarget::UpdateClient(_)
)
}
fn effective_value<'a>(&self, f: &'a FormField) -> Option<&'a str> {
let trimmed = f.value.as_deref().unwrap_or("").trim();
if trimmed.is_empty() {
return None;
}
if self.is_update() && trimmed == f.prefill.trim() {
return None;
}
Some(trimmed)
}
pub fn into_server_form(&self) -> Result<ServerForm> {
if self.is_client() {
anyhow::bail!("into_server_form called on a client form");
}
let mut form = ServerForm {
protocol: self.protocol.clone(),
..Default::default()
};
let mut params = serde_json::Map::new();
for f in &self.fields {
let v = match self.effective_value(f) {
Some(v) => v,
None => continue, };
apply_common_field(f, v, &mut params, |t, val| match t {
FieldTarget::Port => {
form.port = Some(parse_port(val, &f.name)?);
Ok(())
}
FieldTarget::Instruction => {
form.instruction = Some(val.to_string());
Ok(())
}
FieldTarget::Memory => {
form.initial_memory = Some(val.to_string());
Ok(())
}
FieldTarget::EventHandlers => {
form.event_handlers = Some(parse_handlers(val)?);
Ok(())
}
FieldTarget::FeedbackInstructions => {
form.feedback_instructions = Some(val.to_string());
Ok(())
}
FieldTarget::RemoteAddr => Ok(()), FieldTarget::StartupParam { .. } => unreachable!(),
})?;
}
if !params.is_empty() {
form.startup_params = Some(Value::Object(params));
}
Ok(form)
}
pub fn into_client_form(&self) -> Result<ClientForm> {
if !self.is_client() {
anyhow::bail!("into_client_form called on a server form");
}
let mut form = ClientForm {
protocol: self.protocol.clone(),
..Default::default()
};
let mut params = serde_json::Map::new();
for f in &self.fields {
let v = match self.effective_value(f) {
Some(v) => v,
None => continue,
};
apply_common_field(f, v, &mut params, |t, val| match t {
FieldTarget::RemoteAddr => {
form.remote_addr = Some(val.to_string());
Ok(())
}
FieldTarget::Instruction => {
form.instruction = Some(val.to_string());
Ok(())
}
FieldTarget::Memory => {
form.initial_memory = Some(val.to_string());
Ok(())
}
FieldTarget::EventHandlers => {
form.event_handlers = Some(parse_handlers(val)?);
Ok(())
}
FieldTarget::FeedbackInstructions => {
form.feedback_instructions = Some(val.to_string());
Ok(())
}
FieldTarget::Port => Ok(()), FieldTarget::StartupParam { .. } => unreachable!(),
})?;
}
if !params.is_empty() {
form.startup_params = Some(Value::Object(params));
}
Ok(form)
}
}
fn common_server_fields(current: &ServerPrefill, update: bool) -> Vec<FormField> {
let (port, instr, mem, fb) = if update {
(
current.port.to_string(),
current.instruction.clone(),
current.memory.clone(),
current.feedback_instructions.clone().unwrap_or_default(),
)
} else {
("0".to_string(), String::new(), String::new(), String::new())
};
vec![
field(
"port",
"u16",
"Port to bind (0 = OS-assigns a free port)",
port,
FieldTarget::Port,
),
field(
"instruction",
"string",
"Natural-language LLM instruction (fallback handling path)",
instr,
FieldTarget::Instruction,
),
field(
"initial_memory",
"string",
"Seed the instance's LLM memory",
mem,
FieldTarget::Memory,
),
field(
"event_handlers",
"json array",
"Deterministic handlers, e.g. [{\"event_pattern\":\"http_request\",\"handler\":{\"type\":\"static\",...}}]",
if update { handlers_to_prefill(¤t.event_handlers) } else { String::new() },
FieldTarget::EventHandlers,
),
field(
"feedback_instructions",
"string",
"Instructions for the automatic feedback loop",
fb,
FieldTarget::FeedbackInstructions,
),
]
}
fn common_client_fields(current: &ClientPrefill, update: bool) -> Vec<FormField> {
let (instr, mem, fb) = if update {
(
current.instruction.clone(),
current.memory.clone(),
current.feedback_instructions.clone().unwrap_or_default(),
)
} else {
(String::new(), String::new(), String::new())
};
let mut remote = field(
"remote_addr",
"string",
"Remote server address, host:port",
if update {
current.remote_addr.clone()
} else {
String::new()
},
FieldTarget::RemoteAddr,
);
remote.required = !update;
vec![
remote,
field(
"instruction",
"string",
"Natural-language LLM instruction (fallback handling path)",
instr,
FieldTarget::Instruction,
),
field(
"initial_memory",
"string",
"Seed the instance's LLM memory",
mem,
FieldTarget::Memory,
),
field(
"event_handlers",
"json array",
"Deterministic handlers (JSON array)",
if update {
handlers_to_prefill(¤t.event_handlers)
} else {
String::new()
},
FieldTarget::EventHandlers,
),
field(
"feedback_instructions",
"string",
"Instructions for the automatic feedback loop",
fb,
FieldTarget::FeedbackInstructions,
),
]
}
fn apply_common_field(
f: &FormField,
v: &str,
params: &mut serde_json::Map<String, Value>,
set_common: impl FnOnce(&FieldTarget, &str) -> Result<()>,
) -> Result<()> {
if let FieldTarget::StartupParam { type_hint } = &f.target {
params.insert(f.name.clone(), coerce_param_value(v, type_hint, &f.name)?);
Ok(())
} else {
set_common(&f.target, v)
}
}
fn parse_port(v: &str, name: &str) -> Result<u16> {
v.parse::<u16>().map_err(|_| {
anyhow::anyhow!(
"field '{}' must be a port number 0-65535, got '{}'",
name,
v
)
})
}
fn parse_handlers(v: &str) -> Result<Vec<Value>> {
let parsed: Value = serde_json::from_str(v)
.map_err(|e| anyhow::anyhow!("field 'event_handlers' is not valid JSON: {}", e))?;
parsed
.as_array()
.cloned()
.ok_or_else(|| anyhow::anyhow!("field 'event_handlers' must be a JSON array"))
}
fn coerce_param_value(raw: &str, type_hint: &str, name: &str) -> Result<Value> {
let hint = type_hint.to_lowercase();
if hint.contains("bool") {
return raw
.parse::<bool>()
.map(Value::Bool)
.map_err(|_| anyhow::anyhow!("field '{}' must be true/false, got '{}'", name, raw));
}
if hint.contains("int")
|| hint.contains("number")
|| hint.contains("u16")
|| hint.contains("u32")
{
if let Ok(n) = raw.parse::<i64>() {
return Ok(Value::from(n));
}
if let Ok(fl) = raw.parse::<f64>() {
return Ok(Value::from(fl));
}
anyhow::bail!("field '{}' must be a number, got '{}'", name, raw);
}
if hint.contains("object") || hint.contains("array") || hint.contains("json") {
return serde_json::from_str(raw)
.map_err(|e| anyhow::anyhow!("field '{}' must be valid JSON: {}", name, e));
}
Ok(Value::String(raw.to_string()))
}