slice 3.5d: agent_template_link + brain seed helper + skills merge
ci / gates (push) Successful in 4s
ci / frontend (push) Successful in 38s
ci / rust (push) Failing after 3m1s
ci / e2e (push) Skipped
ci / publish (push) Skipped

Ships the lineage layer that ties agents back to their team template
and wires the MCP skills server to actually merge template default
skills with per-agent overrides.

Migration 0050 adds `agent_template_link` (agent_id PK, template_id,
template_version, role_slot, seeded_at, created_at + indexes for
template/role lookups). Populated at agent-materialization time by
Slice 4's mission-launch orchestrator; read here by the skills MCP
server and by future level-up (Slice 8.5).

New Rust surface:
  - cm_db::repo::agent_template_link  (upsert / get / mark_seeded /
    agents_for_template — the last is what level-up's "prompt upgrade
    on template N+1" query needs)
  - cm_api::brain_seed::ingest(claw_id, seed_md, identity_prompt)
    opens cm_brain::ClawBrain on spawn_blocking, sets system_prompt
    on first touch, writes seed as agent_md, commits. Idempotent —
    skips when agent_md already populated.
  - cm_api::mcp_skills::mcp_skills tools/call now resolves the caller
    agent's template + role via agent_template_link and merges
    template default skills with per-agent overrides (was overrides-
    only in Slice 3.5b).
  - cm_api::team_template_loader now binds template_role_skills after
    upserting each template — looks up each declared skill by name,
    attaches with pin_in_context=true for foundation skills and the
    first two role skills. Missing skills log + skip.
  - Boot ordering: skills load BEFORE team templates so the binding
    lookup resolves.

Follow-up (Slice 4): mission-launch orchestrator calls brain_seed::ingest
+ agent_template_link::upsert when minting a team from a template.
Until that lands, the link is populated only by manual writes; the
MCP merge is silent-no-op for agents without a link (falls through
to overrides-only), which matches the pre-3.5d behavior.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
This commit is contained in:
Omar Sobh
2026-07-19 14:16:16 -07:00
co-authored by Claude Opus 4.7
parent 520a717dec
commit 85a97dffca
8 changed files with 290 additions and 20 deletions
+71
View File
@@ -0,0 +1,71 @@
//! Brain-seed ingestion helper — Slice 3.5d.
//!
//! Called at agent-materialization time (from a template role, or
//! from any future code path that wants to prime a fresh brain).
//! Reads the role's `brain_seed` markdown, opens the agent's HDF5
//! brain file via `cm_brain::ClawBrain`, and writes the seed as
//! `agent_md`.
//!
//! `agent_md` is the right slot (not `system_prompt`) because:
//! - system_prompt is the agent's identity + role — comes from
//! the template's role.system_prompt field, ingested separately
//! - agent_md is the "durable knowledge base" the agent reads
//! each session — exactly the semantics of a seed
//!
//! Idempotent: no-op if the brain file already has non-empty agent_md.
//! Level-up (Slice 8.5) diffs current agent_md against the template's
//! current-version brain_seed to propose consolidation.
use std::path::PathBuf;
use uuid::Uuid;
fn brain_dir() -> PathBuf {
std::env::var("CLAWMATES_BRAIN_DIR")
.ok()
.map(PathBuf::from)
.unwrap_or_else(|| PathBuf::from("/data/brains"))
}
/// Ingest `seed_md` into agent `claw_id`'s brain. Also sets
/// `system_prompt` from `identity_prompt` if the brain doesn't have
/// one yet (first-touch seed mirrors the load-time seeding in the
/// existing routes::claws::load_brain path).
///
/// Runs cm-brain on `spawn_blocking` — HDF5 I/O is sync + can be slow.
pub async fn ingest(claw_id: Uuid, seed_md: String, identity_prompt: String) -> Result<(), String> {
tokio::task::spawn_blocking(move || -> Result<(), String> {
use cm_brain::ClawBrain;
let dir = brain_dir();
std::fs::create_dir_all(&dir).map_err(|e| format!("mkdir brain dir: {e}"))?;
let path = dir.join(format!("claw_{claw_id}.h5"));
let mut brain = ClawBrain::open_or_create(&path, &claw_id.to_string())
.map_err(|e| format!("open brain: {e}"))?;
// First-touch identity seed.
if brain.system_prompt().is_none() && !identity_prompt.trim().is_empty() {
brain
.set_system_prompt(&identity_prompt)
.map_err(|e| format!("set system_prompt: {e}"))?;
}
// Skip if agent_md already populated — respect prior state.
// Level-up is the right path to overwrite; not this helper.
if brain
.agent_md()
.map(|s| !s.trim().is_empty())
.unwrap_or(false)
{
return Ok(());
}
brain
.set_agent_md(&seed_md)
.map_err(|e| format!("set agent_md: {e}"))?;
brain
.commit(Some("brain_seed: initial seed from template role"))
.map_err(|e| format!("commit: {e}"))?;
Ok(())
})
.await
.map_err(|e| format!("brain seed task join: {e}"))?
}