use anda_cognitive_nexus::CognitiveNexus;
use anda_core::{BoxError, FunctionDefinition, Resource, Tool, ToolOutput};
use anda_engine::{context::BaseCtx, memory::MemoryManagement};
use anda_kip::DefineKind;
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_DEFINES_PER_REQUEST: usize = 8;
pub const MAX_SYMBOL_CHARS: usize = 64;
#[derive(Debug, Clone, Default, PartialEq)]
pub struct MemoryVocabulary {
pub types: BTreeSet<String>,
pub predicates: BTreeSet<String>,
pub draft_types: BTreeSet<String>,
pub draft_predicates: BTreeSet<String>,
pub revision: 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, drafted, borrowed) in [
(
"LIST TYPES LIMIT 1000",
&mut vocabulary.types,
&mut vocabulary.draft_types,
&mut vocabulary.borrowed_types,
),
(
"LIST PREDICATES LIMIT 1000",
&mut vocabulary.predicates,
&mut vocabulary.draft_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 reference == anda_kip::DRAFT_PACKAGE_REF {
drafted.insert(name.to_string());
} else 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);
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(any(test, feature = "wiki"))]
pub fn covers<'a>(
&self,
types: impl IntoIterator<Item = &'a str>,
predicates: impl IntoIterator<Item = &'a str>,
) -> bool {
types.into_iter().all(|name| self.has_type(name))
&& predicates.into_iter().all(|name| self.has_predicate(name))
}
fn has_type(&self, name: &str) -> bool {
self.types.contains(name)
|| self.draft_types.contains(name)
|| self.borrowed_types.contains(name)
}
fn has_predicate(&self, name: &str) -> bool {
self.predicates.contains(name)
|| self.draft_predicates.contains(name)
|| self.borrowed_predicates.contains(name)
}
pub fn len(&self) -> usize {
self.package_len() + self.draft_types.len() + self.draft_predicates.len()
}
pub fn package_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.package_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('_')
}
pub fn kind_label(kind: DefineKind) -> &'static str {
match kind {
DefineKind::ConceptType => "ConceptType",
DefineKind::Predicate => "PredicateType",
}
}
pub fn is_symbol_name(kind: DefineKind, name: &str) -> bool {
match kind {
DefineKind::ConceptType => is_type_name(name),
DefineKind::Predicate => is_predicate_name(name),
}
}
fn host_description(kind: DefineKind, name: &str) -> String {
match kind {
DefineKind::ConceptType => 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."
),
DefineKind::Predicate => format!(
"Relation `{name}`, met while forming this Space's memory. A claim under it is \
somebody's statement, never a verified fact."
),
}
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct Drafted {
pub defined: Vec<String>,
pub rejected: Vec<String>,
}
pub async fn draft_symbols(
nexus: &CognitiveNexus,
types: &[&str],
predicates: &[&str],
) -> Result<Drafted, BoxError> {
let vocabulary = MemoryVocabulary::load(nexus).await?;
let mut drafted = Drafted::default();
let mut size = vocabulary.len();
let mut seen = BTreeSet::new();
for (kind, names) in [
(DefineKind::ConceptType, types),
(DefineKind::Predicate, predicates),
] {
for &name in names {
if !seen.insert((kind_label(kind), name)) {
continue;
}
let known = match kind {
DefineKind::ConceptType => vocabulary.has_type(name),
DefineKind::Predicate => vocabulary.has_predicate(name),
};
if known {
continue;
}
if !is_symbol_name(kind, name) || size >= MAX_SYMBOLS {
drafted.rejected.push(name.to_string());
continue;
}
let description = host_description(kind, name);
let command = match kind {
DefineKind::ConceptType => "DEFINE CONCEPT TYPE :name {description: :description}",
DefineKind::Predicate => "DEFINE PREDICATE :name {description: :description}",
};
let response = anda_kip::execute_request(
nexus,
&kip::request_with(
command,
Map::from_iter([
("name".to_string(), Json::from(name)),
("description".to_string(), Json::from(description.as_str())),
]),
),
)
.await;
if kip::error_of(&response).is_some_and(|error| error.code == "SchemaSymbolConflict") {
continue;
}
let Some(reference) = kip::ok_result(&response)
.and_then(|result| result.get("ref"))
.and_then(Json::as_str)
.map(str::to_string)
else {
return Err(format!(
"defining {} `{name}` failed: {}",
kind_label(kind),
kip::error_message(&response)
)
.into());
};
size += 1;
queue_schema_review(nexus, kind, &reference, &description).await?;
drafted.defined.push(reference);
}
}
Ok(drafted)
}
pub(crate) async fn queue_schema_review(
executor: &impl anda_kip::Executor,
kind: DefineKind,
reference: &str,
description: &str,
) -> Result<(), BoxError> {
let label = kind_label(kind);
let name = reference.rsplit('/').next().unwrap_or(reference);
let summary: String = format!("Review the draft {label} `{name}`: {description}")
.chars()
.take(1024)
.collect();
let response = anda_kip::execute_request(
executor,
&kip::request_with(
r#"CREATE CONCEPT ?task {
TYPE "SleepTask"
CLIENT KEY :key
NAME :name
SET ATTRIBUTES { task_class: "review_schema", summary: :summary, status: "pending", created_at: :now, symbol_kind: :kind, symbol_ref: :ref }
}"#,
Map::from_iter([
(
"key".to_string(),
Json::from(format!("review_schema:{label}:{reference}")),
),
(
"name".to_string(),
Json::from(format!("Review draft {label} {name}")),
),
("summary".to_string(), Json::from(summary)),
(
"now".to_string(),
Json::from(kip::timestamp(anda_engine::unix_ms())),
),
("kind".to_string(), Json::from(label)),
("ref".to_string(), Json::from(reference)),
]),
),
)
.await;
if kip::succeeded(&response) {
Ok(())
} else {
Err(format!(
"queueing the review of {reference} failed: {}",
kip::error_message(&response)
)
.into())
}
}
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct ModelDefine {
pub index: usize,
pub kind: DefineKind,
pub description: String,
}
pub(crate) fn model_defines(request: &anda_kip::Request) -> Vec<ModelDefine> {
request
.operations
.iter()
.enumerate()
.filter_map(|(index, operation)| {
let anda_kip::Command::Kml(statement) = operation.parse().ok()? else {
return None;
};
let [anda_kip::MutationClause::Define(define)] = statement.clauses.as_slice() else {
return None;
};
let description = match define.definition.get("description") {
Some(anda_kip::BoundValue::Value(anda_kip::KipValue::String(text))) => text.clone(),
_ => String::new(),
};
Some(ModelDefine {
index,
kind: define.kind,
description,
})
})
.collect()
}
pub(crate) async fn check_define_budget(
nexus: &CognitiveNexus,
count: usize,
) -> Result<(), String> {
let vocabulary = MemoryVocabulary::load(nexus)
.await
.map_err(|error| error.to_string())?;
if vocabulary.len() + count > MAX_SYMBOLS {
return Err(format!(
"this Space's vocabulary holds {} of its {MAX_SYMBOLS} symbols; reuse an existing \
symbol instead of defining another",
vocabulary.len()
));
}
Ok(())
}
pub(crate) async fn review_model_defines(
nexus: &CognitiveNexus,
defines: &[ModelDefine],
response: &anda_kip::Response,
) {
for define in defines {
let Some(reference) = response
.results
.get(define.index)
.filter(|result| result.error.is_none())
.and_then(|result| result.result.as_ref())
.and_then(|result| result.get("ref"))
.and_then(Json::as_str)
else {
continue;
};
if let Err(error) =
queue_schema_review(nexus, define.kind, reference, &define.description).await
{
log::warn!(target: "brain", "{error}");
}
}
}
#[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": "Deprecated shortcut for DEFINE: drafts bare Concept type and predicate \
names with a generic host description, for when you cannot write one. \
Prefer `DEFINE CONCEPT TYPE` / `DEFINE PREDICATE` through execute_kip \
with a real description. KIP 2.0 resolves every symbol through the \
Space's Schema Environment, so a command naming an undefined type or \
predicate is refused with SchemaSymbolNotFound. 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. \
Defining 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())
}
#[derive(Clone)]
pub struct DeclareSymbolsTool {
memory: Arc<MemoryManagement>,
product_control: Option<Arc<crate::product::control::Control>>,
lock: Arc<tokio::sync::Mutex<()>>,
}
impl DeclareSymbolsTool {
pub const NAME: &'static str = "declare_memory_symbols";
pub fn new(memory: Arc<MemoryManagement>) -> Self {
Self {
memory,
product_control: None,
lock: Arc::new(tokio::sync::Mutex::new(())),
}
}
pub(crate) fn with_product_control(
mut self,
control: Arc<crate::product::control::Control>,
) -> Self {
self.lock = control.vocabulary_gate.clone();
self.product_control = Some(control);
self
}
pub async fn declare(
&self,
args: &DeclareSymbolsArgs,
) -> Result<(Json, Vec<String>), BoxError> {
let _guard = self.lock.lock().await;
let nexus = self.memory.nexus();
let types: Vec<&str> = args.types.iter().map(String::as_str).collect();
let predicates: Vec<&str> = args.predicates.iter().map(String::as_str).collect();
let drafted = draft_symbols(nexus.as_ref(), &types, &predicates).await?;
Ok((
json!({
"draft_package": anda_kip::DRAFT_PACKAGE_REF,
"defined": drafted.defined,
}),
drafted.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> {
if ctx.agent != crate::agents::FormationAgent::NAME {
return Err(
"only Formation drafts vocabulary; Maintenance reviews drafts and \
records a near-synonym as an Insight about the existing symbol"
.into(),
);
}
let _guard = if let Some(control) = &self.product_control {
let guard = control.gate.read().await;
control.check(&ctx)?;
Some(guard)
} else {
None
};
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 a_space_without_a_host_package_names_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 drafts_legacy_symbols_and_borrowed_ones_all_resolve() {
let vocabulary = MemoryVocabulary {
types: BTreeSet::from(["Project".to_string()]),
draft_types: BTreeSet::from(["Instrument".to_string()]),
draft_predicates: BTreeSet::from(["mentors".to_string()]),
borrowed_types: BTreeSet::from(["Person".to_string()]),
borrowed_predicates: BTreeSet::from(["prefers".to_string()]),
revision: 3,
..Default::default()
};
assert!(vocabulary.covers(["Project", "Instrument", "Person"], ["mentors", "prefers"]));
assert!(!vocabulary.covers(["Drug"], []));
assert_eq!(vocabulary.len(), 3);
assert_eq!(vocabulary.package_len(), 1);
assert_eq!(vocabulary.package_ref(), "kip://anda-brain/memory@1.0.3");
}
#[test]
fn symbol_names_follow_their_kind() {
assert!(is_symbol_name(DefineKind::ConceptType, "MedicalDevice"));
assert!(is_symbol_name(DefineKind::Predicate, "works_on"));
for (kind, name) in [
(DefineKind::ConceptType, "drug"),
(DefineKind::ConceptType, "medical device"),
(DefineKind::ConceptType, "Assertion"),
(DefineKind::Predicate, "Treats"),
(DefineKind::Predicate, "ends_"),
] {
assert!(!is_symbol_name(kind, name), "{name}");
}
assert!(!is_type_name(&"A".repeat(MAX_SYMBOL_CHARS + 1)));
}
#[test]
fn the_legacy_artifact_declares_exactly_the_symbols_it_holds() {
let vocabulary = MemoryVocabulary {
types: BTreeSet::from(["Drug".to_string(), "Symptom".to_string()]),
predicates: BTreeSet::from(["treats".to_string()]),
draft_types: BTreeSet::from(["Instrument".to_string()]),
revision: 1,
..Default::default()
};
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 a_model_define_is_read_with_its_position_and_description() {
let mut request = kip::request("DESCRIBE PRIMER");
request.operations.extend(
kip::request(
r#"DEFINE PREDICATE "mentors" {description: "The subject mentors the object."}"#,
)
.operations,
);
request.operations.extend(
kip::request(r#"DEFINE CONCEPT TYPE "Instrument" {description: "An instrument."}"#)
.operations,
);
assert_eq!(
model_defines(&request),
vec![
ModelDefine {
index: 1,
kind: DefineKind::Predicate,
description: "The subject mentors the object.".into(),
},
ModelDefine {
index: 2,
kind: DefineKind::ConceptType,
description: "An instrument.".into(),
},
]
);
}
}