perf(chat): index skills in the prompt instead of inlining every body

The chat path concatenated every installed skill's complete markdown into
the system prompt on every turn. Bodies average ~3.5 KB (~900 tokens) and the
count is unbounded, so this was by far the largest thing in the prompt and it
scaled with how many skills a claw had installed -- a fixed toll paid whether
or not any skill was relevant to the turn.

The prompt now lists name + description, and a new `skills.read` tool fetches
a body on demand. This is the contract the mission path already had: the
`clawmates_skills` MCP server advertises description + when_to_use and lets
the agent read what it needs. The two paths now agree.

`compose_system` takes (title, description, body) rather than (title, body):
the index needs the description, and first-touch brain seeding still needs the
real body so the .brain stays a complete portable artifact.

Not done here: filtering tool descriptors per agent, which the plan paired
with this. The premise doesn't hold -- risk_profile governs the ZeroClaw tool
namespace (file_edit, shell) on the mission path, while the chat path has its
own registry (files.write, shell.exec) and no per-agent policy whatsoever;
`risk_profile` appears nowhere in cm-runtime. Filtering there would invent a
capability boundary rather than enforce one, silently revoking chat tools.
Left for a deliberate decision.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
Omar Sobh
2026-07-30 10:47:27 -07:00
co-authored by Claude Opus 5
parent 285d0c82f2
commit 81b93a5c25
4 changed files with 121 additions and 16 deletions
+23 -10
View File
@@ -1,10 +1,12 @@
//! 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 user's turn, and compose a system prompt
//! that injects the claw's **identity** (AGENTS.md "how I operate" + personality),
//! **skills**, and **recalled memory** on top of its Postgres-authoritative system
//! prompt. Any failure falls back to the plain prompt — the brain must never break chat.
//! 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.
@@ -25,7 +27,7 @@ fn brain_dir() -> PathBuf {
pub fn compose_system(
agent_id: &str,
base_prompt: &str,
skills: &[(String, String)], // (title, body)
skills: &[(String, String, String)], // (title, description, body)
user_text: &str,
session_label: &str,
) -> String {
@@ -41,7 +43,7 @@ pub fn compose_system(
fn try_compose(
agent_id: &str,
base_prompt: &str,
skills: &[(String, String)],
skills: &[(String, String, String)],
user_text: &str,
session_label: &str,
) -> Result<String, cm_brain::BrainError> {
@@ -55,7 +57,7 @@ fn try_compose(
if !base_prompt.is_empty() {
brain.set_system_prompt(base_prompt)?;
}
for (name, body) in skills {
for (name, _description, body) in skills {
brain.set_skill(name, body)?;
}
}
@@ -86,10 +88,21 @@ fn try_compose(
out.push_str("\n\n## Personality\n");
out.push_str(&persona);
}
// 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 (apply them when relevant)\n");
for (name, body) in skills {
out.push_str(&format!("\n### {name}\n{body}\n"));
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() {