use anyhow::{Context, Result, bail};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs;
use std::path::Path;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(untagged)]
pub enum StateValue {
Float(f32),
Enum(String),
Nested(HashMap<String, StateValue>),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DynamicState {
pub schema_id: String,
pub values: HashMap<String, StateValue>,
}
impl DynamicState {
pub fn encode_stele(&self, schema: &StateSchema) -> String {
let s = &schema.stele;
let mut parts = vec![format!("{}:{}", s.header, schema.name)];
let mut dim_names: Vec<_> = schema.dimensions.keys().collect();
dim_names.sort();
for dim_name in dim_names {
if let Some(dim_def) = schema.dimensions.get(dim_name.as_str())
&& let Some(value) = self.values.get(dim_name.as_str())
{
Self::encode_dimension(
&mut parts,
dim_name,
dim_def,
value,
&s.symbols,
&s.modality_values,
&s.separator,
&s.nested_separator,
"",
);
}
}
parts.join(&s.separator)
}
#[allow(clippy::too_many_arguments)]
#[allow(clippy::only_used_in_recursion)]
fn encode_dimension(
parts: &mut Vec<String>,
name: &str,
definition: &Dimension,
value: &StateValue,
symbols: &HashMap<String, String>,
modality_values: &HashMap<String, String>,
separator: &str,
nested_separator: &str,
prefix: &str,
) {
let symbol = symbols.get(name).map(|s| s.as_str()).unwrap_or(name);
match (definition, value) {
(Dimension::Float { .. }, StateValue::Float(v)) => {
parts.push(format!("{}{}{}", prefix, symbol, v));
}
(Dimension::Enum { .. }, StateValue::Enum(v)) => {
let value_sym = modality_values.get(v).map(|s| s.as_str()).unwrap_or(v);
parts.push(format!("{}{}{}", prefix, symbol, value_sym));
}
(Dimension::Nested { dimensions, .. }, StateValue::Nested(nested_values)) => {
let new_prefix = if prefix.is_empty() {
format!("{}{}", symbol, nested_separator)
} else {
format!("{}{}{}", prefix, symbol, nested_separator)
};
let mut nested_names: Vec<_> = dimensions.keys().collect();
nested_names.sort();
for nested_name in nested_names {
if let Some(nested_def) = dimensions.get(nested_name.as_str())
&& let Some(nested_value) = nested_values.get(nested_name.as_str())
{
Self::encode_dimension(
parts,
nested_name,
nested_def,
nested_value,
symbols,
modality_values,
separator,
nested_separator,
&new_prefix,
);
}
}
}
_ => {
}
}
}
pub fn decode_stele(stele: &str, schema: &StateSchema) -> Result<Self> {
let s = &schema.stele;
let sep = &s.separator;
let nsep = &s.nested_separator;
let mut top_level_sym_to_name: HashMap<&str, &str> = HashMap::new();
let mut nested_parent_syms: std::collections::HashSet<&str> =
std::collections::HashSet::new();
for (name, dim) in &schema.dimensions {
if let Some(sym) = s.symbols.get(name.as_str()) {
top_level_sym_to_name.insert(sym.as_str(), name.as_str());
if matches!(dim, Dimension::Nested { .. }) {
nested_parent_syms.insert(sym.as_str());
}
}
}
let mut rev_modality: HashMap<&str, &str> = HashMap::new();
for (name, sym) in &s.modality_values {
rev_modality.insert(sym.as_str(), name.as_str());
}
let parts: Vec<&str> = stele.split(sep).collect();
let mut values: HashMap<String, StateValue> = HashMap::new();
for part in parts.iter().skip(1) {
if part.is_empty() {
continue;
}
let mut is_nested = false;
let mut parent_sym_len = 0;
for parent_sym in &nested_parent_syms {
let pattern = format!("{}{}", parent_sym, nsep);
if part.starts_with(&pattern) {
is_nested = true;
parent_sym_len = parent_sym.len();
break;
}
}
if is_nested {
let parent_part = &part[..parent_sym_len];
let child_part = &part[parent_sym_len + nsep.len()..];
if let Some(&parent_name) = top_level_sym_to_name.get(parent_part)
&& let Some(Dimension::Nested { dimensions, .. }) =
schema.dimensions.get(parent_name)
{
let mut child_sym_to_name: HashMap<&str, &str> = HashMap::new();
for child_name in dimensions.keys() {
if let Some(child_sym) = s.symbols.get(child_name.as_str()) {
child_sym_to_name.insert(child_sym.as_str(), child_name.as_str());
}
}
for (child_sym, &child_name) in &child_sym_to_name {
if let Some(value_str) = child_part.strip_prefix(child_sym) {
let nested = values
.entry(parent_name.to_string())
.or_insert_with(|| StateValue::Nested(HashMap::new()));
if let StateValue::Nested(nested_map) = nested
&& let Some(child_dim) = dimensions.get(child_name)
{
match child_dim {
Dimension::Float { .. } => {
if let Ok(v) = value_str.parse::<f32>() {
nested_map.insert(
child_name.to_string(),
StateValue::Float(v),
);
}
}
Dimension::Enum { .. } => {
let enum_val = rev_modality
.get(value_str)
.map(|s| s.to_string())
.unwrap_or_else(|| value_str.to_string());
nested_map.insert(
child_name.to_string(),
StateValue::Enum(enum_val),
);
}
_ => {}
}
}
break;
}
}
}
} else {
for (sym, &name) in &top_level_sym_to_name {
if let Some(value_str) = part.strip_prefix(sym) {
if let Some(dim) = schema.dimensions.get(name) {
match dim {
Dimension::Float { .. } => {
if let Ok(v) = value_str.parse::<f32>() {
values.insert(name.to_string(), StateValue::Float(v));
}
}
Dimension::Enum { .. } => {
let enum_val = rev_modality
.get(value_str)
.map(|s| s.to_string())
.unwrap_or_else(|| value_str.to_string());
values.insert(name.to_string(), StateValue::Enum(enum_val));
}
Dimension::Nested { .. } => {
}
}
}
break;
}
}
}
}
Ok(DynamicState {
schema_id: schema.title.clone(),
values,
})
}
pub fn from_mode(mode: &str, schema: &StateSchema) -> Result<Self> {
let mapping = schema
.mode_mappings
.get(mode)
.or_else(|| schema.mode_mappings.get("default"))
.context("No mode mapping found and no default defined")?;
let values = mapping.clone();
Ok(DynamicState {
schema_id: schema.title.clone(),
values,
})
}
pub fn describe(&self, schema: &StateSchema) -> String {
let mut parts = Vec::new();
let mut dim_names: Vec<_> = self.values.keys().collect();
dim_names.sort();
for dim_name in dim_names {
if let Some(value) = self.values.get(dim_name.as_str())
&& let Some(dim_def) = schema.dimensions.get(dim_name.as_str())
{
let desc = Self::describe_value(dim_name, dim_def, value);
if !desc.is_empty() {
parts.push(desc);
}
}
}
parts.join(", ")
}
fn describe_value(name: &str, definition: &Dimension, value: &StateValue) -> String {
match (definition, value) {
(Dimension::Float { hints, .. }, StateValue::Float(v)) => {
let hint_name = if hints.is_empty() {
String::new()
} else {
let mut closest = ("", f32::MAX);
for (hint_name, hint_val) in hints {
let distance = (v - hint_val).abs();
if distance < closest.1 {
closest = (hint_name.as_str(), distance);
}
}
closest.0.to_string()
};
if hint_name.is_empty() {
format!("{}: {:.1}", name, v)
} else {
format!("{}: {} ({:.1})", name, hint_name, v)
}
}
(Dimension::Enum { .. }, StateValue::Enum(v)) => {
format!("{}: {}", name, v)
}
(Dimension::Nested { dimensions, .. }, StateValue::Nested(nested_values)) => {
let mut nested_parts = Vec::new();
let mut nested_names: Vec<_> = nested_values.keys().collect();
nested_names.sort();
for nested_name in nested_names {
if let Some(nested_value) = nested_values.get(nested_name.as_str())
&& let Some(nested_def) = dimensions.get(nested_name.as_str())
{
let desc = Self::describe_value(nested_name, nested_def, nested_value);
if !desc.is_empty() {
nested_parts.push(desc);
}
}
}
if nested_parts.is_empty() {
String::new()
} else {
format!("{}: [{}]", name, nested_parts.join(", "))
}
}
_ => String::new(),
}
}
pub fn interactive_capture(schema: &StateSchema) -> Result<Self> {
use std::io::{self, Write};
fn prompt_float(prompt: &str, hints: &HashMap<String, f32>) -> Result<f32> {
let hint_str: Vec<String> = hints
.iter()
.map(|(k, v)| format!("{}={:.1}", k, v))
.collect();
print!("{} [{}]: ", prompt, hint_str.join(", "));
io::stdout().flush()?;
let mut input = String::new();
io::stdin().read_line(&mut input)?;
let input = input.trim();
if let Some(&val) = hints.get(input) {
return Ok(val);
}
input.parse().context("Expected number or hint word")
}
fn prompt_enum(prompt: &str, values: &[String]) -> Result<String> {
print!("{} [{}]: ", prompt, values.join("/"));
io::stdout().flush()?;
let mut input = String::new();
io::stdin().read_line(&mut input)?;
let input = input.trim().to_lowercase();
if values.iter().any(|v| v.to_lowercase() == input) {
Ok(input)
} else {
bail!("Expected one of: {}", values.join(", "))
}
}
fn capture_dimension(_name: &str, definition: &Dimension) -> Result<StateValue> {
match definition {
Dimension::Float { prompt, hints, .. } => {
let value = prompt_float(prompt, hints)?;
Ok(StateValue::Float(value))
}
Dimension::Enum { prompt, values, .. } => {
let value = prompt_enum(prompt, values)?;
Ok(StateValue::Enum(value))
}
Dimension::Nested {
description,
dimensions,
} => {
println!("\n{}", description);
let mut nested_values = HashMap::new();
let mut nested_names: Vec<_> = dimensions.keys().collect();
nested_names.sort();
for nested_name in nested_names {
if let Some(nested_def) = dimensions.get(nested_name.as_str()) {
let nested_value = capture_dimension(nested_name, nested_def)?;
nested_values.insert(nested_name.to_string(), nested_value);
}
}
Ok(StateValue::Nested(nested_values))
}
}
}
println!("\n{}: {}\n", schema.title, schema.description);
let mut values = HashMap::new();
let mut dim_names: Vec<_> = schema.dimensions.keys().collect();
dim_names.sort();
for dim_name in dim_names {
if let Some(dim_def) = schema.dimensions.get(dim_name.as_str()) {
let value = capture_dimension(dim_name, dim_def)?;
values.insert(dim_name.to_string(), value);
}
}
Ok(DynamicState {
schema_id: schema.title.clone(),
values,
})
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct SteleConfig {
pub header: String,
pub separator: String,
pub nested_separator: String,
pub symbols: HashMap<String, String>,
pub modality_values: HashMap<String, String>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct DimensionHints {
#[serde(flatten)]
pub values: HashMap<String, f32>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(tag = "type")]
pub enum Dimension {
#[serde(rename = "float")]
Float {
range: [f32; 2],
description: String,
prompt: String,
#[serde(default)]
hints: HashMap<String, f32>,
},
#[serde(rename = "enum")]
Enum {
values: Vec<String>,
description: String,
prompt: String,
},
#[serde(rename = "nested")]
Nested {
description: String,
dimensions: HashMap<String, Dimension>,
},
}
pub type ModeMapping = HashMap<String, StateValue>;
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct StateSchema {
pub title: String,
pub description: String,
pub version: String,
#[serde(rename = "type")]
pub schema_type: String,
pub name: String,
pub stele: SteleConfig,
pub dimensions: HashMap<String, Dimension>,
pub mode_mappings: HashMap<String, ModeMapping>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EmotionalState {
pub temperature: f32,
pub entropy: f32,
pub gravity: f32,
pub depth: f32,
pub energy: f32,
pub toward: TowardState,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TowardState {
pub agency: f32,
pub flow: f32,
pub distance: f32,
pub modality: String,
}
impl EmotionalState {
pub fn to_dynamic(&self) -> DynamicState {
let mut values = HashMap::new();
values.insert(
"temperature".to_string(),
StateValue::Float(self.temperature),
);
values.insert("entropy".to_string(), StateValue::Float(self.entropy));
values.insert("gravity".to_string(), StateValue::Float(self.gravity));
values.insert("depth".to_string(), StateValue::Float(self.depth));
values.insert("energy".to_string(), StateValue::Float(self.energy));
let mut toward_values = HashMap::new();
toward_values.insert("agency".to_string(), StateValue::Float(self.toward.agency));
toward_values.insert("flow".to_string(), StateValue::Float(self.toward.flow));
toward_values.insert(
"distance".to_string(),
StateValue::Float(self.toward.distance),
);
toward_values.insert(
"modality".to_string(),
StateValue::Enum(self.toward.modality.clone()),
);
values.insert("toward".to_string(), StateValue::Nested(toward_values));
DynamicState {
schema_id: "q-state".to_string(),
values,
}
}
pub fn from_dynamic(state: &DynamicState) -> Result<Self> {
fn get_float(values: &HashMap<String, StateValue>, key: &str) -> Result<f32> {
match values.get(key) {
Some(StateValue::Float(v)) => Ok(*v),
_ => bail!("Missing or invalid float value for key: {}", key),
}
}
fn get_enum(values: &HashMap<String, StateValue>, key: &str) -> Result<String> {
match values.get(key) {
Some(StateValue::Enum(v)) => Ok(v.clone()),
_ => bail!("Missing or invalid enum value for key: {}", key),
}
}
fn get_nested<'a>(
values: &'a HashMap<String, StateValue>,
key: &str,
) -> Result<&'a HashMap<String, StateValue>> {
match values.get(key) {
Some(StateValue::Nested(v)) => Ok(v),
_ => bail!("Missing or invalid nested value for key: {}", key),
}
}
let temperature = get_float(&state.values, "temperature")?;
let entropy = get_float(&state.values, "entropy")?;
let gravity = get_float(&state.values, "gravity")?;
let depth = get_float(&state.values, "depth")?;
let energy = get_float(&state.values, "energy")?;
let toward_values = get_nested(&state.values, "toward")?;
let agency = get_float(toward_values, "agency")?;
let flow = get_float(toward_values, "flow")?;
let distance = get_float(toward_values, "distance")?;
let modality = get_enum(toward_values, "modality")?;
Ok(Self {
temperature,
entropy,
gravity,
depth,
energy,
toward: TowardState {
agency,
flow,
distance,
modality,
},
})
}
pub fn from_mode(mode: &str, schema: &StateSchema) -> Result<Self> {
let dynamic = DynamicState::from_mode(mode, schema)?;
Self::from_dynamic(&dynamic)
}
pub fn encode_stele(&self, schema: &StateSchema) -> String {
let s = &schema.stele;
let sym_temp = s
.symbols
.get("temperature")
.map(|s| s.as_str())
.unwrap_or("T");
let sym_ent = s.symbols.get("entropy").map(|s| s.as_str()).unwrap_or("E");
let sym_grav = s.symbols.get("gravity").map(|s| s.as_str()).unwrap_or("G");
let sym_depth = s.symbols.get("depth").map(|s| s.as_str()).unwrap_or("D");
let sym_energy = s.symbols.get("energy").map(|s| s.as_str()).unwrap_or("N");
let sym_toward = s.symbols.get("toward").map(|s| s.as_str()).unwrap_or(">");
let sym_agency = s.symbols.get("agency").map(|s| s.as_str()).unwrap_or("A");
let sym_flow = s.symbols.get("flow").map(|s| s.as_str()).unwrap_or("F");
let sym_dist = s.symbols.get("distance").map(|s| s.as_str()).unwrap_or("I");
let sym_mod = s.symbols.get("modality").map(|s| s.as_str()).unwrap_or("M");
let mod_sym = s
.modality_values
.get(&self.toward.modality)
.map(|s| s.as_str())
.unwrap_or(&self.toward.modality);
let sep = &s.separator;
let nsep = &s.nested_separator;
format!(
"{}:{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}",
s.header,
schema.name,
sep, sym_temp,
self.temperature,
sep, sym_ent,
self.entropy,
sep, sym_grav,
self.gravity,
sep, sym_depth,
self.depth,
sep, sym_energy,
self.energy,
sep, sym_toward,
nsep,
sym_agency,
self.toward.agency,
sep, sym_toward,
nsep,
sym_flow,
self.toward.flow,
sep, sym_toward,
nsep,
sym_dist,
self.toward.distance,
sep, sym_toward,
nsep,
sym_mod,
mod_sym )
}
pub fn decode_stele(stele: &str, schema: &StateSchema) -> Result<Self> {
let s = &schema.stele;
let sep = &s.separator;
let nsep = &s.nested_separator;
let mut sym_to_name: HashMap<&str, &str> = HashMap::new();
for (name, sym) in &s.symbols {
sym_to_name.insert(sym.as_str(), name.as_str());
}
let mut rev_modality: HashMap<&str, &str> = HashMap::new();
for (name, sym) in &s.modality_values {
rev_modality.insert(sym.as_str(), name.as_str());
}
let sym_toward = s.symbols.get("toward").map(|s| s.as_str()).unwrap_or(">");
let parts: Vec<&str> = stele.split(sep).collect();
let mut temperature = 0.5f32;
let mut entropy = 0.5f32;
let mut gravity = 0.5f32;
let mut depth = 0.5f32;
let mut energy = 0.5f32;
let mut agency = 0.5f32;
let mut flow = 0.5f32;
let mut distance = 0.5f32;
let mut modality = String::from("blended");
for part in parts.iter().skip(1) {
if part.starts_with(sym_toward) && part[sym_toward.len()..].starts_with(nsep) {
let after_prefix = &part[sym_toward.len() + nsep.len()..];
for (sym, name) in &sym_to_name {
if let Some(value_str) = after_prefix.strip_prefix(sym) {
match *name {
"agency" => {
if let Ok(v) = value_str.parse() {
agency = v;
}
}
"flow" => {
if let Ok(v) = value_str.parse() {
flow = v;
}
}
"distance" => {
if let Ok(v) = value_str.parse() {
distance = v;
}
}
"modality" => {
modality = rev_modality
.get(value_str)
.map(|s| s.to_string())
.unwrap_or_else(|| value_str.to_string());
}
_ => {}
}
break;
}
}
} else {
for (sym, name) in &sym_to_name {
if let Some(value_str) = part.strip_prefix(sym) {
if let Ok(v) = value_str.parse() {
match *name {
"temperature" => temperature = v,
"entropy" => entropy = v,
"gravity" => gravity = v,
"depth" => depth = v,
"energy" => energy = v,
_ => {}
}
}
break;
}
}
}
}
Ok(Self {
temperature,
entropy,
gravity,
depth,
energy,
toward: TowardState {
agency,
flow,
distance,
modality,
},
})
}
pub fn describe(&self) -> String {
let temp_desc = match self.temperature {
t if t < 0.3 => "cold",
t if t < 0.5 => "cool",
t if t < 0.7 => "warm",
_ => "hot",
};
let entropy_desc = match self.entropy {
e if e < 0.3 => "clear",
e if e < 0.6 => "mixed",
_ => "chaotic",
};
let gravity_desc = match self.gravity {
g if g < 0.4 => "being",
g if g < 0.6 => "balanced",
_ => "building",
};
let depth_desc = match self.depth {
d if d < 0.4 => "surface",
d if d < 0.7 => "middle",
_ => "cosmic",
};
let energy_desc = match self.energy {
e if e < 0.4 => "slow",
e if e < 0.7 => "steady",
_ => "crackling",
};
let agency_desc = match self.toward.agency {
a if a < 0.4 => "receiving",
a if a < 0.6 => "balanced",
_ => "acting",
};
let flow_desc = match self.toward.flow {
f if f < 0.4 => "taking",
f if f < 0.6 => "balanced",
_ => "giving",
};
let distance_desc = match self.toward.distance {
d if d < 0.3 => "merge",
d if d < 0.5 => "close",
d if d < 0.7 => "comfortable",
_ => "observing",
};
format!(
"{} ({:.1}), {} ({:.1}), {} pull ({:.1}), {} depth ({:.1}), {} ({:.1}), \
{} ({:.1}), {} ({:.1}), {} ({:.1}), {} modality",
temp_desc,
self.temperature,
entropy_desc,
self.entropy,
gravity_desc,
self.gravity,
depth_desc,
self.depth,
energy_desc,
self.energy,
agency_desc,
self.toward.agency,
flow_desc,
self.toward.flow,
distance_desc,
self.toward.distance,
self.toward.modality
)
}
pub fn closest_mode(&self, schema: &StateSchema) -> String {
let mut best_mode = String::from("default");
let mut best_distance = f32::MAX;
let self_dynamic = self.to_dynamic();
for (mode_name, mapping) in &schema.mode_mappings {
let mut distance = 0.0f32;
for (key, self_val) in &self_dynamic.values {
if let Some(map_val) = mapping.get(key) {
distance += match (self_val, map_val) {
(StateValue::Float(s), StateValue::Float(m)) => (s - m).powi(2),
(StateValue::Nested(s), StateValue::Nested(m)) => {
let mut nested_dist = 0.0f32;
for (nk, nsv) in s {
if let Some(nmv) = m.get(nk.as_str())
&& let (StateValue::Float(ns), StateValue::Float(nm)) =
(nsv, nmv)
{
nested_dist += (ns - nm).powi(2);
}
}
nested_dist
}
_ => 0.0,
};
}
}
if distance < best_distance {
best_distance = distance;
best_mode = mode_name.clone();
}
}
best_mode
}
}
pub fn load_schema(path: &Path) -> Result<StateSchema> {
let content = fs::read_to_string(path)
.with_context(|| format!("Failed to read schema file: {:?}", path))?;
serde_json::from_str(&content).with_context(|| format!("Failed to parse schema: {:?}", path))
}
pub fn load_default_schema() -> Result<StateSchema> {
if let Ok(schema_path) = std::env::var("MX_STATE_SCHEMA") {
let path = std::path::PathBuf::from(&schema_path);
if path.exists() {
return load_schema(&path);
} else {
bail!(
"MX_STATE_SCHEMA points to non-existent file: {}",
schema_path
);
}
}
if let Ok(agent) = std::env::var("MX_CURRENT_AGENT")
&& let Some(home) = dirs::home_dir()
{
let agent_schema = home.join(format!(".{}/schemas/state.json", agent));
if agent_schema.exists() {
return load_schema(&agent_schema);
}
}
let locations = [
Some(crate::paths::schemas_dir().join("emotional-state.json")),
Some(std::path::PathBuf::from(
"/etc/mx/schemas/emotional-state.json",
)),
];
for loc in locations.into_iter().flatten() {
if loc.exists() {
return load_schema(&loc);
}
}
bail!(
"Could not find state schema. Tried:\n\
- MX_STATE_SCHEMA environment variable\n\
- MX_CURRENT_AGENT environment variable (looks for ~/.{{agent}}/schemas/state.json)\n\
- {}/emotional-state.json\n\
- /etc/mx/schemas/emotional-state.json",
crate::paths::schemas_dir().display()
)
}
pub fn parse_wake_preference_dynamic(line: &str, schema: &StateSchema) -> Result<DynamicState> {
let trimmed = line.trim();
if trimmed.starts_with(&schema.stele.header) {
return DynamicState::decode_stele(trimmed, schema);
}
let mode = if let Some(stripped) = trimmed.strip_prefix("Wake Preference:") {
stripped.trim()
} else if let Some(stripped) = trimmed.strip_prefix("Wake State:") {
stripped.trim()
} else {
trimmed
};
DynamicState::from_mode(mode, schema)
}
pub fn parse_wake_preference(line: &str, schema: &StateSchema) -> Result<EmotionalState> {
let trimmed = line.trim();
if trimmed.starts_with(&schema.stele.header) {
return EmotionalState::decode_stele(trimmed, schema);
}
let mode = if let Some(stripped) = trimmed.strip_prefix("Wake Preference:") {
stripped.trim()
} else if let Some(stripped) = trimmed.strip_prefix("Wake State:") {
stripped.trim()
} else {
trimmed
};
EmotionalState::from_mode(mode, schema)
}
pub fn interactive_capture(schema: &StateSchema) -> Result<EmotionalState> {
use std::io::{self, Write};
fn prompt_float(prompt: &str, hints: &HashMap<String, f32>) -> Result<f32> {
let hint_str: Vec<String> = hints
.iter()
.map(|(k, v)| format!("{}={:.1}", k, v))
.collect();
print!("{} [{}]: ", prompt, hint_str.join(", "));
io::stdout().flush()?;
let mut input = String::new();
io::stdin().read_line(&mut input)?;
let input = input.trim();
if let Some(&val) = hints.get(input) {
return Ok(val);
}
input.parse().context("Expected number or hint word")
}
fn prompt_enum(prompt: &str, values: &[String]) -> Result<String> {
print!("{} [{}]: ", prompt, values.join("/"));
io::stdout().flush()?;
let mut input = String::new();
io::stdin().read_line(&mut input)?;
let input = input.trim().to_lowercase();
if values.iter().any(|v| v.to_lowercase() == input) {
Ok(input)
} else {
bail!("Expected one of: {}", values.join(", "))
}
}
let dims = &schema.dimensions;
let temperature = if let Some(Dimension::Float { prompt, hints, .. }) = dims.get("temperature")
{
prompt_float(prompt, hints)?
} else {
0.5
};
let entropy = if let Some(Dimension::Float { prompt, hints, .. }) = dims.get("entropy") {
prompt_float(prompt, hints)?
} else {
0.5
};
let gravity = if let Some(Dimension::Float { prompt, hints, .. }) = dims.get("gravity") {
prompt_float(prompt, hints)?
} else {
0.5
};
let depth = if let Some(Dimension::Float { prompt, hints, .. }) = dims.get("depth") {
prompt_float(prompt, hints)?
} else {
0.5
};
let energy = if let Some(Dimension::Float { prompt, hints, .. }) = dims.get("energy") {
prompt_float(prompt, hints)?
} else {
0.5
};
let (agency, flow, distance, modality) = if let Some(Dimension::Nested { dimensions, .. }) =
dims.get("toward")
{
let agency = if let Some(Dimension::Float { prompt, hints, .. }) = dimensions.get("agency")
{
prompt_float(prompt, hints)?
} else {
0.5
};
let flow = if let Some(Dimension::Float { prompt, hints, .. }) = dimensions.get("flow") {
prompt_float(prompt, hints)?
} else {
0.5
};
let distance =
if let Some(Dimension::Float { prompt, hints, .. }) = dimensions.get("distance") {
prompt_float(prompt, hints)?
} else {
0.5
};
let modality =
if let Some(Dimension::Enum { prompt, values, .. }) = dimensions.get("modality") {
prompt_enum(prompt, values)?
} else {
String::from("blended")
};
(agency, flow, distance, modality)
} else {
(0.5, 0.5, 0.5, String::from("blended"))
};
Ok(EmotionalState {
temperature,
entropy,
gravity,
depth,
energy,
toward: TowardState {
agency,
flow,
distance,
modality,
},
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_mode_to_state() {
let schema_json = r#"{
"title": "Test",
"description": "Test schema",
"version": "1.0.0",
"type": "tensor",
"name": "test",
"stele": {
"header": "@state",
"separator": "|",
"nested_separator": ".",
"symbols": {},
"modality_values": {}
},
"dimensions": {},
"mode_mappings": {
"soft": {
"temperature": 0.6,
"entropy": 0.2,
"gravity": 0.2,
"depth": 0.4,
"energy": 0.3,
"toward": {
"agency": 0.3,
"flow": 0.5,
"distance": 0.2,
"modality": "emotional"
}
},
"default": {
"temperature": 0.5,
"entropy": 0.5,
"gravity": 0.5,
"depth": 0.5,
"energy": 0.5,
"toward": {
"agency": 0.5,
"flow": 0.5,
"distance": 0.5,
"modality": "blended"
}
}
}
}"#;
let schema: StateSchema = serde_json::from_str(schema_json).unwrap();
let state = EmotionalState::from_mode("soft", &schema).unwrap();
assert!((state.temperature - 0.6).abs() < 0.01);
assert!((state.entropy - 0.2).abs() < 0.01);
assert_eq!(state.toward.modality, "emotional");
}
#[test]
fn test_dynamic_bridge_roundtrip() {
let original = EmotionalState {
temperature: 0.7,
entropy: 0.3,
gravity: 0.6,
depth: 0.5,
energy: 0.8,
toward: TowardState {
agency: 0.4,
flow: 0.6,
distance: 0.2,
modality: String::from("emotional"),
},
};
let dynamic = original.to_dynamic();
let decoded = EmotionalState::from_dynamic(&dynamic).unwrap();
assert!((original.temperature - decoded.temperature).abs() < 0.01);
assert!((original.entropy - decoded.entropy).abs() < 0.01);
assert!((original.gravity - decoded.gravity).abs() < 0.01);
assert!((original.depth - decoded.depth).abs() < 0.01);
assert!((original.energy - decoded.energy).abs() < 0.01);
assert!((original.toward.agency - decoded.toward.agency).abs() < 0.01);
assert!((original.toward.flow - decoded.toward.flow).abs() < 0.01);
assert!((original.toward.distance - decoded.toward.distance).abs() < 0.01);
assert_eq!(original.toward.modality, decoded.toward.modality);
}
#[test]
fn test_stele_roundtrip() {
let schema_json = r#"{
"title": "Test",
"description": "Test schema",
"version": "1.0.0",
"type": "tensor",
"name": "test",
"stele": {
"header": "@state",
"separator": "|",
"nested_separator": ".",
"symbols": {
"temperature": "T",
"entropy": "E",
"gravity": "G",
"depth": "D",
"energy": "N",
"toward": ">",
"agency": "A",
"flow": "F",
"distance": "I",
"modality": "M"
},
"modality_values": {
"physical": "P",
"emotional": "E",
"intellectual": "I",
"blended": "B"
}
},
"dimensions": {},
"mode_mappings": {}
}"#;
let schema: StateSchema = serde_json::from_str(schema_json).unwrap();
let original = EmotionalState {
temperature: 0.7,
entropy: 0.3,
gravity: 0.6,
depth: 0.5,
energy: 0.8,
toward: TowardState {
agency: 0.4,
flow: 0.6,
distance: 0.2,
modality: String::from("emotional"),
},
};
let stele = original.encode_stele(&schema);
let decoded = EmotionalState::decode_stele(&stele, &schema).unwrap();
assert!((original.temperature - decoded.temperature).abs() < 0.01);
assert!((original.entropy - decoded.entropy).abs() < 0.01);
assert_eq!(original.toward.modality, decoded.toward.modality);
}
}
#[cfg(test)]
mod dynamic_state_tests {
use super::*;
fn get_q_schema() -> StateSchema {
let schema_json = include_str!("../schemas/example-q-state.json");
serde_json::from_str(schema_json).unwrap()
}
#[test]
fn test_q_encode_decode_roundtrip() {
let schema = get_q_schema();
let mut values = HashMap::new();
values.insert("temperature".to_string(), StateValue::Float(0.7));
values.insert("entropy".to_string(), StateValue::Float(0.3));
values.insert("gravity".to_string(), StateValue::Float(0.6));
values.insert("depth".to_string(), StateValue::Float(0.5));
values.insert("energy".to_string(), StateValue::Float(0.8));
let mut toward = HashMap::new();
toward.insert("agency".to_string(), StateValue::Float(0.4));
toward.insert("flow".to_string(), StateValue::Float(0.6));
toward.insert("distance".to_string(), StateValue::Float(0.2));
toward.insert(
"modality".to_string(),
StateValue::Enum("emotional".to_string()),
);
values.insert("toward".to_string(), StateValue::Nested(toward));
let original = DynamicState {
schema_id: "q-state".to_string(),
values,
};
let stele = original.encode_stele(&schema);
println!("Q Stele: {}", stele);
let decoded = DynamicState::decode_stele(&stele, &schema).unwrap();
assert_eq!(
decoded.values.len(),
original.values.len(),
"Should have same number of dimensions"
);
for (key, orig_val) in &original.values {
let dec_val = decoded
.values
.get(key)
.unwrap_or_else(|| panic!("Decoded state missing key: {}", key));
match (orig_val, dec_val) {
(StateValue::Float(o), StateValue::Float(d)) => {
assert!(
(o - d).abs() < 0.01,
"Float mismatch for {}: {} vs {}",
key,
o,
d
);
}
(StateValue::Enum(o), StateValue::Enum(d)) => {
assert_eq!(o, d, "Enum mismatch for {}: {} vs {}", key, o, d);
}
(StateValue::Nested(o), StateValue::Nested(d)) => {
for (nested_key, nested_orig) in o {
let nested_dec = d.get(nested_key.as_str()).unwrap_or_else(|| {
panic!("Decoded state missing nested key: {}.{}", key, nested_key)
});
match (nested_orig, nested_dec) {
(StateValue::Float(no), StateValue::Float(nd)) => {
assert!(
(no - nd).abs() < 0.01,
"Nested float mismatch for {}.{}: {} vs {}",
key,
nested_key,
no,
nd
);
}
(StateValue::Enum(no), StateValue::Enum(nd)) => {
assert_eq!(
no, nd,
"Nested enum mismatch for {}.{}: {} vs {}",
key, nested_key, no, nd
);
}
_ => panic!("Type mismatch for nested {}.{}", key, nested_key),
}
}
}
_ => panic!("Type mismatch for {}", key),
}
}
}
#[test]
fn test_q_from_mode() {
let schema = get_q_schema();
let state = DynamicState::from_mode("soft", &schema).unwrap();
assert!(state.values.contains_key("temperature"));
assert!(state.values.contains_key("toward"));
if let Some(StateValue::Nested(toward)) = state.values.get("toward") {
assert!(toward.contains_key("modality"));
} else {
panic!("Toward missing or wrong type");
}
}
#[test]
fn test_q_describe() {
let schema = get_q_schema();
let state = DynamicState::from_mode("soft", &schema).unwrap();
let description = state.describe(&schema);
println!("Q Description: {}", description);
assert!(description.contains("temperature"));
assert!(description.contains("toward"));
}
#[test]
fn test_soren_encode_decode_roundtrip() {
let schema_json = std::fs::read_to_string("schemas/example-soren-state.json")
.expect("Failed to read Soren schema");
let schema: StateSchema =
serde_json::from_str(&schema_json).expect("Failed to parse Soren schema");
let original =
DynamicState::from_mode("tending", &schema).expect("Failed to create state from mode");
let stele = original.encode_stele(&schema);
println!("Soren stele: {}", stele);
let decoded = DynamicState::decode_stele(&stele, &schema).expect("Failed to decode stele");
assert_eq!(
decoded.values.len(),
original.values.len(),
"Should have same number of dimensions"
);
for (key, orig_val) in &original.values {
let dec_val = decoded
.values
.get(key)
.unwrap_or_else(|| panic!("Decoded state missing key: {}", key));
match (orig_val, dec_val) {
(StateValue::Float(o), StateValue::Float(d)) => {
assert!(
(o - d).abs() < 0.01,
"Float mismatch for {}: {} vs {}",
key,
o,
d
);
}
(StateValue::Nested(o), StateValue::Nested(d)) => {
for (nested_key, nested_orig) in o {
let nested_dec = d.get(nested_key.as_str()).unwrap_or_else(|| {
panic!("Decoded state missing nested key: {}.{}", key, nested_key)
});
match (nested_orig, nested_dec) {
(StateValue::Float(no), StateValue::Float(nd)) => {
assert!(
(no - nd).abs() < 0.01,
"Nested float mismatch for {}.{}: {} vs {}",
key,
nested_key,
no,
nd
);
}
_ => panic!("Type mismatch for nested {}.{}", key, nested_key),
}
}
}
_ => panic!("Type mismatch for {}", key),
}
}
}
#[test]
fn test_soren_from_mode() {
let schema_json = std::fs::read_to_string("schemas/example-soren-state.json")
.expect("Failed to read Soren schema");
let schema: StateSchema =
serde_json::from_str(&schema_json).expect("Failed to parse Soren schema");
let state =
DynamicState::from_mode("tending", &schema).expect("Failed to create state from mode");
assert!(state.values.contains_key("ground"));
assert!(state.values.contains_key("threshold"));
assert!(state.values.contains_key("tending"));
assert!(state.values.contains_key("carrying"));
if let Some(StateValue::Nested(carrying)) = state.values.get("carrying") {
assert!(carrying.contains_key("threads"));
assert!(carrying.contains_key("weight"));
assert!(carrying.contains_key("proximity"));
} else {
panic!("Carrying missing or wrong type");
}
}
#[test]
fn test_soren_describe() {
let schema_json = std::fs::read_to_string("schemas/example-soren-state.json")
.expect("Failed to read Soren schema");
let schema: StateSchema =
serde_json::from_str(&schema_json).expect("Failed to parse Soren schema");
let state =
DynamicState::from_mode("tending", &schema).expect("Failed to create state from mode");
let description = state.describe(&schema);
println!("Soren Description: {}", description);
assert!(description.contains("ground"));
assert!(description.contains("threshold"));
assert!(description.contains("tending"));
assert!(description.contains("carrying"));
}
}