mod formation;
mod maintenance;
pub mod prompts;
mod recall;
use anda_core::{BoxError, ContentPart, Document, FunctionDefinition, Message, Principal, Usage};
use anda_db::schema::DocumentId;
use anda_engine::{
context::CompletionRunner,
memory::{Conversation, ConversationStatus},
unix_ms,
};
use parking_lot::RwLock;
use serde_json::json;
use std::{collections::VecDeque, sync::LazyLock};
pub use formation::*;
pub use maintenance::*;
pub use recall::*;
pub(crate) static KIP_FUNCTION_DEFINITION: LazyLock<FunctionDefinition> = LazyLock::new(|| {
serde_json::from_value(json!({
"name": "execute_kip",
"description": "Executes one or more KIP (Knowledge Interaction Protocol) commands against the Cognitive Nexus to interact with your persistent memory.",
"parameters": {
"type": "object",
"properties": {
"commands": {
"type": "array",
"description": "An array of KIP commands for batch execution (reduces round-trips). Commands are executed sequentially; execution stops on first KML error.",
"items": {
"type": "string"
}
},
"parameters": {
"type": "object",
"description": "An optional JSON object of key-value pairs used for safe substitution of placeholders in the command string(s). Placeholders should start with ':' (e.g., :name, :limit). IMPORTANT: A placeholder must represent a complete JSON value token (e.g., name: :name). Do not embed placeholders inside quoted strings (e.g., \"Hello :name\"), because substitution uses JSON serialization."
},
},
"required": ["commands"]
},
"strict": true
})).unwrap()
});
#[async_trait::async_trait]
pub trait BrainHook: Send + Sync {
fn is_maintenance_processing(&self) -> bool;
async fn on_conversation_end(&self, agent_name: &str, conversation: &Conversation);
async fn try_start_formation(&self);
async fn try_start_maintenance(&self, formation_id: DocumentId) -> Option<DocumentId>;
}
pub static SELF_USER_ID: Principal = Principal::from_slice(&[1]);
const COMPACTION_CONTINUE_PROMPT: &str = "Continue the active memory-agent work from the compaction handoff. The handoff contains the conversation state immediately before compaction.";
pub(super) const PERSIST_EVERY_N_TURNS: usize = 5;
pub(super) const RUNNER_MAX_MODEL_TURNS: usize = 200;
pub(super) const RUNNER_MAX_WALL_CLOCK_MS: u64 = 30 * 60 * 1000;
pub(super) enum RunnerFlow {
Continue,
Break,
}
pub(super) trait RunnerHost {
fn label(&self) -> &'static str;
fn history(&self) -> &RwLock<VecDeque<Document>>;
async fn persist_snapshot(&self, conversation: &Conversation);
async fn mark_failed(&self, conversation: &mut Conversation, reason: String);
fn turn_is_done(&self, runner: &CompletionRunner) -> bool;
fn on_turn_success(&self, conversation: &mut Conversation);
fn after_turn(&mut self, runner: &mut CompletionRunner, is_done: bool) -> RunnerFlow;
}
pub(super) async fn drive_runner_loop<H: RunnerHost>(
host: &mut H,
runner: &mut CompletionRunner,
conversation: &mut Conversation,
) {
let started_at_ms = unix_ms();
let mut replace_initial_input = true;
let mut persisted_runner_history_len = 0;
let mut total_model_turns = 0usize;
let mut accounted_runner_turns = 0usize;
let mut unpersisted_turns = 0usize;
let failure: Option<String> = 'run: {
loop {
if total_model_turns >= RUNNER_MAX_MODEL_TURNS {
break 'run Some(format!(
"{} exceeded model turn limit of {}",
host.label(),
RUNNER_MAX_MODEL_TURNS
));
}
if unix_ms().saturating_sub(started_at_ms) >= RUNNER_MAX_WALL_CLOCK_MS {
break 'run Some(format!(
"{} exceeded wall-clock budget of {} seconds",
host.label(),
RUNNER_MAX_WALL_CLOCK_MS / 1000
));
}
match compact_runner_if_needed(runner).await {
Ok(true) => {
total_model_turns = total_model_turns.saturating_add(1);
accounted_runner_turns = runner.turns();
persisted_runner_history_len = 0;
replace_initial_input = false;
}
Ok(false) => {}
Err(err) => break 'run Some(format!("CompletionRunner error: {err:?}")),
}
match runner.next().await {
Ok(None) => break 'run None,
Ok(Some(res)) => {
let runner_turns = runner.turns();
total_model_turns = total_model_turns
.saturating_add(runner_turns.saturating_sub(accounted_runner_turns));
accounted_runner_turns = runner_turns;
let now_ms = unix_ms();
let is_done = host.turn_is_done(runner);
append_runner_history(
conversation,
&res.chat_history,
&mut persisted_runner_history_len,
&mut replace_initial_input,
);
conversation.status = if res.failed_reason.is_some() {
ConversationStatus::Failed
} else if is_done {
ConversationStatus::Completed
} else {
ConversationStatus::Working
};
conversation.usage = res.usage;
conversation.updated_at = now_ms;
if let Some(failed_reason) = res.failed_reason {
conversation.failed_reason = Some(failed_reason);
} else {
host.on_turn_success(conversation);
push_completed_history(host.history(), conversation, 2);
}
unpersisted_turns = unpersisted_turns.saturating_add(1);
if conversation.status != ConversationStatus::Working
|| unpersisted_turns >= PERSIST_EVERY_N_TURNS
{
host.persist_snapshot(conversation).await;
unpersisted_turns = 0;
}
if conversation.status == ConversationStatus::Failed {
break 'run None;
}
if let RunnerFlow::Break = host.after_turn(runner, is_done) {
break 'run None;
}
}
Err(err) => break 'run Some(format!("CompletionRunner error: {err:?}")),
}
}
};
if let Some(reason) = failure {
conversation.usage = runner.total_usage().clone();
host.mark_failed(conversation, reason).await;
}
if unpersisted_turns > 0 && conversation.status == ConversationStatus::Working {
host.persist_snapshot(conversation).await;
}
}
fn queued_runner_tokens(runner: &CompletionRunner) -> u64 {
runner
.steering_message_iter()
.chain(runner.follow_up_message_iter())
.map(|part| part.estimated_tokens() as u64)
.sum()
}
pub(super) async fn compact_runner_if_needed(
runner: &mut CompletionRunner,
) -> Result<bool, BoxError> {
if runner.is_done() {
return Ok(false);
}
if !runner.needs_compaction_with(|| queued_runner_tokens(runner)) {
return Ok(false);
}
let pre_usage = runner.total_usage().clone();
let pre_tools_usage = runner.tools_usage().clone();
match runner.handoff(None).await {
Ok((mut compacted, output)) => {
compacted.accumulate(&output.usage);
compacted.accumulate_tools_usage(&output.tools_usage);
compacted.follow_up(ContentPart::from(COMPACTION_CONTINUE_PROMPT.to_string()));
*runner = compacted;
Ok(true)
}
Err(err) => {
if usage_is_empty(runner.total_usage()) {
runner.accumulate(&pre_usage);
}
if runner.tools_usage().is_empty() {
runner.accumulate_tools_usage(&pre_tools_usage);
}
Err(err)
}
}
}
fn usage_is_empty(usage: &Usage) -> bool {
usage.input_tokens == 0
&& usage.output_tokens == 0
&& usage.cached_tokens == 0
&& usage.requests == 0
}
pub(super) fn push_completed_history(
history: &RwLock<VecDeque<Document>>,
conversation: &Conversation,
max_len: usize,
) {
if conversation.status != ConversationStatus::Completed || max_len == 0 {
return;
}
let doc: Document = conversation.clone().into();
let mut history = history.write();
history.push_back(doc);
let len = history.len();
if len > max_len {
history.drain(0..(len - max_len));
}
}
pub(super) fn append_runner_history(
conversation: &mut Conversation,
chat_history: &[Message],
persisted_runner_history_len: &mut usize,
replace_existing: &mut bool,
) {
if chat_history.is_empty() {
return;
}
if *replace_existing {
conversation.messages.clear();
*replace_existing = false;
}
let incoming_len = chat_history.len();
let new_messages = if incoming_len >= *persisted_runner_history_len {
chat_history[*persisted_runner_history_len..].to_vec()
} else {
chat_history.to_vec()
};
conversation.append_messages(new_messages);
*persisted_runner_history_len = incoming_len;
}
#[cfg(test)]
mod tests {
use super::{append_runner_history, push_completed_history};
use anda_core::Message;
use anda_engine::memory::{Conversation, ConversationStatus};
use parking_lot::RwLock;
use std::collections::VecDeque;
#[test]
fn push_completed_history_ignores_working_conversations_and_caps_length() {
let history = RwLock::new(VecDeque::new());
let mut conversation = Conversation {
_id: 1,
status: ConversationStatus::Working,
..Default::default()
};
push_completed_history(&history, &conversation, 2);
assert!(history.read().is_empty());
conversation.status = ConversationStatus::Completed;
push_completed_history(&history, &conversation, 2);
conversation._id = 2;
push_completed_history(&history, &conversation, 2);
conversation._id = 3;
push_completed_history(&history, &conversation, 2);
assert_eq!(history.read().len(), 2);
}
#[test]
fn append_runner_history_appends_after_runner_reset_without_clearing() {
let mut conversation = Conversation::default();
let mut persisted_runner_history_len = 0;
let mut replace_existing = true;
conversation.append_messages(vec![Message {
role: "user".to_string(),
content: vec!["original input".to_string().into()],
..Default::default()
}]);
append_runner_history(
&mut conversation,
&[Message {
role: "assistant".to_string(),
content: vec!["first runner draft".to_string().into()],
..Default::default()
}],
&mut persisted_runner_history_len,
&mut replace_existing,
);
assert_eq!(conversation.messages.len(), 1);
persisted_runner_history_len = 0;
replace_existing = false;
append_runner_history(
&mut conversation,
&[Message {
role: "assistant".to_string(),
content: vec!["compacted runner summary".to_string().into()],
..Default::default()
}],
&mut persisted_runner_history_len,
&mut replace_existing,
);
let messages = serde_json::to_string(&conversation.messages).unwrap();
assert!(messages.contains("first runner draft"));
assert!(messages.contains("compacted runner summary"));
assert!(!messages.contains("original input"));
}
}