use anda_cognitive_nexus::CognitiveNexus;
use anda_core::{BoxError, FunctionDefinition, Resource, Tool, ToolOutput};
use anda_engine::{context::BaseCtx, memory::MemoryManagement};
use serde::Deserialize;
use serde_json::{Map, Value as Json, json};
use std::{
collections::BTreeSet,
sync::{Arc, LazyLock},
};
use crate::kip;
pub const MEMORY_PACKAGE_ID: &str = "kip://anda-brain/memory";
pub const MAX_SYMBOLS: usize = 512;
pub const MAX_SYMBOL_CHARS: usize = 64;
#[derive(Debug, Clone, Default, PartialEq)]
pub struct MemoryVocabulary {
pub types: BTreeSet<String>,
pub predicates: BTreeSet<String>,
pub revision: u32,
floor: u32,
borrowed_types: BTreeSet<String>,
borrowed_predicates: BTreeSet<String>,
}
impl MemoryVocabulary {
pub async fn load(nexus: &CognitiveNexus) -> Result<Self, BoxError> {
let mut vocabulary = Self::default();
let mut version = String::new();
for (command, mine, borrowed) in [
(
"LIST TYPES LIMIT 1000",
&mut vocabulary.types,
&mut vocabulary.borrowed_types,
),
(
"LIST PREDICATES LIMIT 1000",
&mut vocabulary.predicates,
&mut vocabulary.borrowed_predicates,
),
] {
let response = anda_kip::execute_request(nexus, &kip::request(command)).await;
if !kip::succeeded(&response) {
return Err(format!(
"reading the Space's vocabulary failed: {}",
kip::error_message(&response)
)
.into());
}
let Some(entries) = kip::ok_result(&response).and_then(Json::as_array) else {
continue;
};
for entry in entries {
let Some(name) = entry.get("local_name").and_then(Json::as_str) else {
continue;
};
let reference = entry
.get("package_ref")
.and_then(Json::as_str)
.unwrap_or_default();
if let Some(declared) = reference.strip_prefix(&format!("{MEMORY_PACKAGE_ID}@")) {
version = declared.to_string();
mine.insert(name.to_string());
} else {
borrowed.insert(name.to_string());
}
}
}
vocabulary.revision = patch_of(&version).unwrap_or(0);
vocabulary.floor = highest_installed_revision(nexus).await?;
Ok(vocabulary)
}
pub fn version(&self) -> String {
format!("1.0.{}", self.revision)
}
pub fn package_ref(&self) -> String {
format!("{MEMORY_PACKAGE_ID}@{}", self.version())
}
#[cfg_attr(not(feature = "wiki"), allow(dead_code))]
pub fn covers<'a>(
&self,
types: impl IntoIterator<Item = &'a str>,
predicates: impl IntoIterator<Item = &'a str>,
) -> bool {
types
.into_iter()
.all(|name| self.types.contains(name) || self.borrowed_types.contains(name))
&& predicates.into_iter().all(|name| {
self.predicates.contains(name) || self.borrowed_predicates.contains(name)
})
}
pub fn extend<'a>(
&mut self,
types: impl IntoIterator<Item = &'a str>,
predicates: impl IntoIterator<Item = &'a str>,
) -> Vec<String> {
let mut rejected = Vec::new();
let mut changed = false;
let mut published = self.len();
for (names, valid, mine, borrowed) in [
(
types.into_iter().collect::<Vec<_>>(),
is_type_name as fn(&str) -> bool,
&mut self.types,
&self.borrowed_types,
),
(
predicates.into_iter().collect::<Vec<_>>(),
is_predicate_name as fn(&str) -> bool,
&mut self.predicates,
&self.borrowed_predicates,
),
] {
for name in names {
if borrowed.contains(name) || mine.contains(name) {
continue;
}
if !valid(name) {
rejected.push(name.to_string());
continue;
}
if published >= MAX_SYMBOLS {
rejected.push(name.to_string());
continue;
}
mine.insert(name.to_string());
published += 1;
changed = true;
}
}
if changed {
self.revision = self.revision.max(self.floor).saturating_add(1);
self.floor = self.revision;
}
rejected
}
pub fn len(&self) -> usize {
self.types.len() + self.predicates.len()
}
pub async fn activate(&self, nexus: &CognitiveNexus) -> Result<(), BoxError> {
let mut artifacts: Vec<(&str, String)> = vec![(
"anda_brain",
anda_cognitive_nexus::profiles::COGNITIVE_MEMORY.to_string(),
)];
if self.len() > 0 {
artifacts.push(("anda_brain_memory", self.artifact()));
}
let artifacts: Vec<(&str, &str)> = artifacts
.iter()
.map(|(source, artifact)| (*source, artifact.as_str()))
.collect();
nexus
.install_and_activate(&artifacts, anda_cognitive_nexus::nexus::DEFAULT_SPACE)
.await?;
Ok(())
}
pub fn artifact(&self) -> String {
let package_ref = self.package_ref();
let mut concept_types = Map::new();
for name in &self.types {
concept_types.insert(
name.clone(),
json!({
"ref": format!("{package_ref}/{name}"),
"kind": "ConceptType",
"description": format!(
"Entity type `{name}`, met while forming this Space's memory. It means \
whatever the sources it came from meant by it; nothing here verifies that."
),
"attributes": {"open": true, "fields": {}},
}),
);
}
let mut predicates = Map::new();
for name in &self.predicates {
predicates.insert(
name.clone(),
json!({
"ref": format!("{package_ref}/{name}"),
"kind": "PredicateType",
"description": format!(
"Relation `{name}`, met while forming this Space's memory. A claim under \
it is somebody's statement, never a verified fact."
),
"subject": {"kinds": ["Concept"]},
"object": {"kinds": ["Concept"]},
"functional": false,
"open_world": true,
"complete": false,
}),
);
}
let package = json!({
"format": "KIP-Schema-Package",
"format_version": "2.0-draft",
"manifest": {
"package_id": MEMORY_PACKAGE_ID,
"version": self.version(),
"package_ref": package_ref,
"name": "Anda Brain space vocabulary",
"description": "Concept types and predicates this Space's memory needed beyond the \
Cognitive Memory Profile. Deployment-local: these symbols mean what \
their sources meant and are not portable ontology.",
"publisher": "urn:kip:publisher:anda-brain",
"purpose": "deployment_extension",
"stability": "experimental",
"executable": false,
},
"dependencies": [{
"package_id": "kip://core",
"version": "2.0.0",
"package_ref": "kip://core@2.0.0",
"required": true,
}],
"definitions": {
"concept_types": concept_types,
"predicates": predicates,
},
"model_hints": {
"provenance_invariant": "A symbol here was proposed by a model reading a \
conversation or a document. Its presence is not evidence \
that the relation it names is real.",
},
});
serde_json::to_string(&package).expect("the vocabulary package serializes")
}
}
pub fn is_type_name(name: &str) -> bool {
name.len() <= MAX_SYMBOL_CHARS
&& name.chars().next().is_some_and(|c| c.is_ascii_uppercase())
&& name.chars().all(|c| c.is_ascii_alphanumeric())
&& !anda_kip::CORE_ELEMENT_KINDS.contains(&name)
}
pub fn is_predicate_name(name: &str) -> bool {
name.len() <= MAX_SYMBOL_CHARS
&& name.chars().next().is_some_and(|c| c.is_ascii_lowercase())
&& name
.chars()
.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_')
&& !name.ends_with('_')
}
#[derive(Debug, Clone, Default, Deserialize)]
pub struct DeclareSymbolsArgs {
#[serde(default)]
pub types: Vec<String>,
#[serde(default)]
pub predicates: Vec<String>,
}
static DECLARE_SYMBOLS_DEFINITION: LazyLock<FunctionDefinition> = LazyLock::new(|| {
serde_json::from_value(json!({
"name": DeclareSymbolsTool::NAME,
"description": "Declares Concept types and predicates this memory needs before you can \
write with them. KIP 2.0 resolves every symbol through the Space's Schema \
Environment, so a KML command naming an undeclared type or predicate is \
refused with SchemaSymbolNotFound — call this first, then write. Reuse an \
existing symbol wherever one fits: `LIST TYPES` and `LIST PREDICATES` show \
what this Space already speaks, and minting a synonym splits one memory in \
two. Declaring a symbol says nothing about whether any claim using it is \
true.",
"parameters": {
"type": "object",
"properties": {
"types": {
"type": "array",
"description": "Concept type names in UpperCamelCase, letters and digits only \
(e.g. Project, MedicalDevice).",
"items": {"type": "string"}
},
"predicates": {
"type": "array",
"description": "Predicate names in snake_case, lowercase letters, digits and \
underscores (e.g. works_on, ships_to).",
"items": {"type": "string"}
}
},
"required": ["types", "predicates"],
"additionalProperties": false
},
"strict": true
}))
.unwrap()
});
fn patch_of(version: &str) -> Option<u32> {
version
.rsplit('.')
.next()
.and_then(|patch| patch.parse().ok())
}
async fn highest_installed_revision(nexus: &CognitiveNexus) -> Result<u32, BoxError> {
let prefix = format!("{MEMORY_PACKAGE_ID}@");
Ok(nexus
.store
.installed_packages()
.await?
.keys()
.filter_map(|package_ref| patch_of(package_ref.strip_prefix(&prefix)?))
.max()
.unwrap_or(0))
}
#[derive(Clone)]
pub struct DeclareSymbolsTool {
memory: Arc<MemoryManagement>,
lock: Arc<tokio::sync::Mutex<()>>,
}
impl DeclareSymbolsTool {
pub const NAME: &'static str = "declare_memory_symbols";
pub fn new(memory: Arc<MemoryManagement>) -> Self {
Self {
memory,
lock: Arc::new(tokio::sync::Mutex::new(())),
}
}
pub async fn declare(
&self,
args: &DeclareSymbolsArgs,
) -> Result<(Json, Vec<String>), BoxError> {
let _guard = self.lock.lock().await;
let nexus = self.memory.nexus();
let mut vocabulary = MemoryVocabulary::load(nexus.as_ref()).await?;
let before = vocabulary.revision;
let rejected = vocabulary.extend(
args.types.iter().map(String::as_str),
args.predicates.iter().map(String::as_str),
);
if vocabulary.revision != before {
vocabulary.activate(nexus.as_ref()).await?;
}
Ok((
json!({
"package_ref": vocabulary.package_ref(),
"types": vocabulary.types,
"predicates": vocabulary.predicates,
}),
rejected,
))
}
}
impl std::fmt::Debug for DeclareSymbolsTool {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("DeclareSymbolsTool").finish_non_exhaustive()
}
}
impl Tool<BaseCtx> for DeclareSymbolsTool {
type Args = DeclareSymbolsArgs;
type Output = Json;
fn name(&self) -> String {
Self::NAME.to_string()
}
fn description(&self) -> String {
DECLARE_SYMBOLS_DEFINITION.description.clone()
}
fn definition(&self) -> FunctionDefinition {
DECLARE_SYMBOLS_DEFINITION.clone()
}
async fn call(
&self,
_ctx: BaseCtx,
args: Self::Args,
_resources: Vec<Resource>,
) -> Result<ToolOutput<Self::Output>, BoxError> {
let (vocabulary, rejected) = self.declare(&args).await?;
let mut output = ToolOutput::new(json!({
"vocabulary": vocabulary,
"rejected": rejected,
"hint": if rejected.is_empty() {
Json::Null
} else {
json!("a rejected name is malformed (types are UpperCamelCase, predicates are \
snake_case) or the Space has reached its symbol cap; reuse an existing \
symbol instead of renaming around the refusal")
},
}));
output.is_error = (!rejected.is_empty()).then_some(true);
Ok(output)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn an_empty_vocabulary_publishes_as_the_first_revision() {
let vocabulary = MemoryVocabulary::default();
assert_eq!(vocabulary.package_ref(), "kip://anda-brain/memory@1.0.0");
assert!(vocabulary.covers([], []));
assert!(!vocabulary.covers(["Drug"], []));
}
#[test]
fn adding_a_symbol_moves_the_version_forward_and_re_adding_does_not() {
let mut vocabulary = MemoryVocabulary::default();
assert!(vocabulary.extend(["Drug"], ["treats"]).is_empty());
assert_eq!(vocabulary.version(), "1.0.1");
assert!(vocabulary.extend(["Drug"], ["treats"]).is_empty());
assert_eq!(vocabulary.version(), "1.0.1");
assert!(vocabulary.covers(["Drug"], ["treats"]));
}
#[test]
fn a_symbol_another_package_declares_is_usable_but_never_redeclared() {
let mut vocabulary = MemoryVocabulary {
borrowed_types: BTreeSet::from(["Person".to_string()]),
borrowed_predicates: BTreeSet::from(["prefers".to_string()]),
..Default::default()
};
assert!(vocabulary.covers(["Person"], ["prefers"]));
assert!(vocabulary.extend(["Person"], ["prefers"]).is_empty());
assert!(vocabulary.types.is_empty());
assert_eq!(vocabulary.revision, 0, "nothing was published");
}
#[test]
fn malformed_names_are_reported_rather_than_published() {
let mut vocabulary = MemoryVocabulary::default();
let rejected = vocabulary.extend(["drug", "medical device", "Ok"], ["Treats", "fine_one"]);
assert_eq!(rejected, vec!["drug", "medical device", "Treats"]);
assert_eq!(vocabulary.types, BTreeSet::from(["Ok".to_string()]));
assert_eq!(
vocabulary.predicates,
BTreeSet::from(["fine_one".to_string()])
);
}
#[test]
fn a_core_kind_is_not_a_concept_type_this_package_may_declare() {
let mut vocabulary = MemoryVocabulary::default();
let rejected = vocabulary.extend(["Assertion", "Evidence", "Project"], []);
assert_eq!(rejected, vec!["Assertion", "Evidence"]);
assert_eq!(vocabulary.types, BTreeSet::from(["Project".to_string()]));
}
#[test]
fn the_cap_counts_this_package_whole_and_nobody_else() {
let mut vocabulary = MemoryVocabulary::default();
vocabulary
.borrowed_types
.extend((0..40).map(|i| format!("Borrowed{i}")));
let types: Vec<String> = (0..MAX_SYMBOLS).map(|i| format!("T{i}")).collect();
let rejected = vocabulary.extend(types.iter().map(String::as_str), []);
assert!(rejected.is_empty(), "{} rejected", rejected.len());
assert_eq!(vocabulary.len(), MAX_SYMBOLS);
let rejected = vocabulary.extend(["OneMore"], ["one_more"]);
assert_eq!(rejected, vec!["OneMore", "one_more"]);
assert_eq!(vocabulary.len(), MAX_SYMBOLS);
}
#[test]
fn a_revision_never_reuses_a_number_already_installed() {
let mut vocabulary = MemoryVocabulary {
revision: 4,
floor: 6,
..Default::default()
};
assert!(vocabulary.extend(["Drug"], []).is_empty());
assert_eq!(vocabulary.version(), "1.0.7");
assert!(vocabulary.extend([], ["treats"]).is_empty());
assert_eq!(vocabulary.version(), "1.0.8");
}
#[test]
fn the_artifact_declares_exactly_the_symbols_it_holds() {
let mut vocabulary = MemoryVocabulary::default();
vocabulary.extend(["Drug", "Symptom"], ["treats"]);
let package: Json = serde_json::from_str(&vocabulary.artifact()).unwrap();
assert_eq!(
package["manifest"]["package_ref"],
"kip://anda-brain/memory@1.0.1"
);
let types = package["definitions"]["concept_types"].as_object().unwrap();
assert_eq!(types.len(), 2);
assert_eq!(
types["Drug"]["ref"],
"kip://anda-brain/memory@1.0.1/Drug".to_string()
);
let predicates = package["definitions"]["predicates"].as_object().unwrap();
assert_eq!(predicates["treats"]["functional"], false);
assert_eq!(predicates["treats"]["open_world"], true);
}
#[test]
fn the_vocabulary_stops_widening_at_its_cap() {
let mut vocabulary = MemoryVocabulary::default();
let names: Vec<String> = (0..MAX_SYMBOLS + 10).map(|i| format!("Type{i}")).collect();
let rejected = vocabulary.extend(names.iter().map(String::as_str), []);
assert_eq!(vocabulary.len(), MAX_SYMBOLS);
assert_eq!(rejected.len(), 10);
}
}