//! Best-effort brain augmentation for the chat path. //! //! Each turn we open the claw's local working `.brain` (cm-brain / ClawhDF5), //! recall relevant memory, record the turn, and compose a system prompt on top //! of the claw's Postgres-authoritative one. Any failure falls back to the plain //! prompt — the brain must never break chat. //! //! Skills are **indexed, not inlined**: the prompt lists what the claw has and //! what each is for, and `skills.read` fetches a body on demand. //! //! The local file is a working cache of the claw's brain (canonical home is //! ClawBrainHub); memory accrues here and is pushed back on save/publish. use std::path::PathBuf; use cm_brain::ClawBrain; /// Where the working `.brain` files live. Shared with `cm_api::mission_memory`, /// which keeps the per-repository brains beside the per-claw ones. pub fn brain_dir() -> PathBuf { std::env::var("CLAWMATES_BRAIN_DIR") .map(PathBuf::from) .unwrap_or_else(|_| std::env::temp_dir().join("clawmates-brains")) } /// Compose the system prompt for a claw, augmenting `base_prompt` with its /// identity (AGENTS.md + personality), skills, and recalled memory, and recording /// the user turn. Best-effort: returns `base_prompt` unchanged on any brain error. pub fn compose_system( agent_id: &str, base_prompt: &str, skills: &[(String, String, String)], // (title, description, body) user_text: &str, session_label: &str, ) -> String { match try_compose(agent_id, base_prompt, skills, user_text, session_label) { Ok(s) => s, Err(e) => { eprintln!("cm-runtime: brain augmentation skipped for {agent_id}: {e}"); base_prompt.to_string() } } } /// Record the assistant's reply in the claw's brain so recall returns whole /// exchanges rather than just the user's half. /// /// Best-effort and silent on failure, like [`compose_system`] — memory is an /// enhancement and must never fail a completed turn. Empty replies (a turn that /// only made tool calls) are skipped so they don't dilute the keyword index. pub fn remember_reply(agent_id: &str, text: &str, session_label: &str) { if text.trim().is_empty() { return; } let path = brain_dir().join(format!("claw_{agent_id}.h5")); match ClawBrain::open_or_create(&path, agent_id) { Ok(mut brain) => { let _ = brain.remember("assistant", text, session_label); } Err(e) => eprintln!("cm-runtime: brain reply-memory skipped for {agent_id}: {e}"), } } fn try_compose( agent_id: &str, base_prompt: &str, skills: &[(String, String, String)], user_text: &str, session_label: &str, ) -> Result { let path = brain_dir().join(format!("claw_{agent_id}.h5")); let mut brain = ClawBrain::open_or_create(&path, agent_id)?; // First touch: seed the brain's definition (Postgres stays authoritative; // the live prompt below always uses the DB values, so this is just so the // .brain is a complete, portable artifact for push-back). if brain.system_prompt().is_none() { if !base_prompt.is_empty() { brain.set_system_prompt(base_prompt)?; } for (name, _description, body) in skills { brain.set_skill(name, body)?; } } let recalled = brain.recall(user_text, 4); // Record the user's turn so future sessions can recall it (best-effort). let _ = brain.remember("user", user_text, session_label); let mut out = String::with_capacity(base_prompt.len() + 512); // Base identity: the Postgres-authoritative system prompt, or — if it's empty // (e.g. a brain pulled from the Hub that carries its identity in soul_md) — // the brain's own system prompt (which falls back to soul_md). if base_prompt.trim().is_empty() { if let Some(sp) = brain.system_prompt() { out.push_str(&sp); } } else { out.push_str(base_prompt); } // `agent_md` ("how I operate") and `personality` are deliberately NOT // injected. Both are standing behavioural instruction — house style, coding // preferences, tone — and their bodies are the team template's `brain_seed` // prose ("prefer let-else over deep nesting", "anti-patterns: unwrap() in // library code"). That is exactly the kind of correction written for weaker // models: a current frontier model either does it unprompted or does it // fine differently, and the text cost a fixed toll on every single turn. // // They remain in the brain, editable from the dashboard and carried in the // portable artifact — this is about what earns a place in the prompt, not // about discarding the data. The claw's DB `system_prompt` still goes in // above: identity and purpose are information, not correction. // Skills are indexed, not inlined. Bodies average ~3.5 KB (~900 tokens) // each and were previously concatenated in full on every turn, unbounded in // the number installed — by far the largest thing in the prompt. The claw // now sees what it has and what each is for, and calls `skills.read` for a // body when one is actually relevant. Same summary-and-fetch contract the // mission path already gets from the `clawmates_skills` MCP server. if !skills.is_empty() { out.push_str("\n\n## Your skills\n"); out.push_str("Call `skills.read` with a skill's name to read it in full.\n"); for (name, description, _body) in skills { if description.trim().is_empty() { out.push_str(&format!("- {name}\n")); } else { out.push_str(&format!("- {name} — {description}\n")); } } } if !recalled.is_empty() { out.push_str("\n\n## Relevant memory from past sessions\n"); for r in &recalled { out.push_str("- "); out.push_str(r); out.push('\n'); } } Ok(out) }