slice 3.5d: agent_template_link + brain seed helper + skills merge
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:
co-authored by
Claude Opus 4.7
parent
520a717dec
commit
85a97dffca
@@ -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}"))?
|
||||
}
|
||||
Reference in New Issue
Block a user