composeSoul — the directory is the agent
Rust · crate toolnexus · SPEC §7E · rust/src/agents/home.rs
pub fn compose_soul(dir: impl AsRef<Path>) -> (String, Vec<String>) // toolnexus::agentspub const BOOTSTRAP_ORDER: [&str; 7] = ["AGENTS.md", "SOUL.md", "IDENTITY.md", "USER.md", "TOOLS.md", "HEARTBEAT.md", "MEMORY.md"];compose_soul is the directory-is-the-agent primitive: it reads BOOTSTRAP_ORDER’s seven
well-known filenames out of dir, in openclaw convention order (identity first, memory last),
and joins each present one into a single soul string as a "## <filename>" section. Absent
files are skipped silently. Each file is read with a 2 MB byte cap — a larger file is truncated
with a notice appended, and the on-disk file is untouched. The returned Vec<String> lists
exactly which files were present, in the order they were composed.
When to use it
Section titled “When to use it”Reach for compose_soul when you’re building a persona-shaped agent by hand — composing the
soul yourself into agents::Spec.soul — rather than going through
from_dir, which calls it for you. It’s also the right call when
you only want the composed prompt text (to log it, hash it for cache-stability checks, or feed
it to a different agent constructor) without building a full Agent.
Why this and not the alternative
Section titled “Why this and not the alternative”Examples
Section titled “Examples”1. The smallest useful call — compose from two files
Section titled “1. The smallest useful call — compose from two files”use toolnexus::agents::compose_soul;
fn main() -> Result<(), Box<dyn std::error::Error>> { let dir = std::env::temp_dir().join(format!("persona-cs1-{}", std::process::id())); std::fs::create_dir_all(&dir)?; std::fs::write(dir.join("SOUL.md"), "You are Kip, a terse assistant.")?; std::fs::write(dir.join("MEMORY.md"), "- prefers metric units")?;
let (soul, found) = compose_soul(&dir); assert_eq!(found, vec!["SOUL.md", "MEMORY.md"]); assert!(soul.contains("## SOUL.md"), "{soul}");
std::fs::remove_dir_all(&dir)?; println!("ok: composed from {found:?}"); Ok(())}2. The realistic case — absent files skip silently, order is fixed
Section titled “2. The realistic case — absent files skip silently, order is fixed”Only AGENTS.md and USER.md exist here; compose_soul still walks the full
BOOTSTRAP_ORDER, composing only what it finds, in that canonical order — not directory
listing order or creation order.
use toolnexus::agents::compose_soul;
fn main() -> Result<(), Box<dyn std::error::Error>> { let dir = std::env::temp_dir().join(format!("persona-cs2-{}", std::process::id())); std::fs::create_dir_all(&dir)?; // Write USER.md first, on disk, but BOOTSTRAP_ORDER still puts AGENTS.md ahead of it. std::fs::write(dir.join("USER.md"), "The user is Muthu.")?; std::fs::write(dir.join("AGENTS.md"), "Reply in one line.")?;
let (soul, found) = compose_soul(&dir); assert_eq!(found, vec!["AGENTS.md", "USER.md"]); assert!(soul.find("AGENTS.md").unwrap() < soul.find("USER.md").unwrap());
std::fs::remove_dir_all(&dir)?; println!("ok: composed {found:?} - SOUL.md/IDENTITY.md/etc were absent and skipped"); Ok(())}3. The full surface — wired into an agent as Spec.soul, and a truncation notice
Section titled “3. The full surface — wired into an agent as Spec.soul, and a truncation notice”compose_soul’s output is exactly what from_dir assigns to agents::Spec.soul — this example
does that assembly explicitly, and also shows the 2 MB truncation notice on an oversized file.
The model is an in-process stand-in (Options.in_process), so nothing leaves the process.
use std::sync::Arc;
use toolnexus::agents::{Options, Spec, compose_soul, new};use toolnexus::client::{GenerateFn, InProcessRequest, InProcessResponse};
#[tokio::main]async fn main() -> Result<(), Box<dyn std::error::Error>> { let dir = std::env::temp_dir().join(format!("persona-cs3-{}", std::process::id())); std::fs::create_dir_all(&dir)?; std::fs::write(dir.join("IDENTITY.md"), "You are Archivist.")?; // One byte over the 2 MB cap: compose_soul truncates with a notice, on-disk file untouched. let oversized = vec![b'x'; 2 * 1024 * 1024 + 1]; std::fs::write(dir.join("TOOLS.md"), &oversized)?;
let (soul, found) = compose_soul(&dir); assert_eq!(found, vec!["IDENTITY.md", "TOOLS.md"]); assert!(soul.contains("[truncated: exceeds 2 MB bootstrap cap]")); assert_eq!(std::fs::read(dir.join("TOOLS.md"))?.len(), oversized.len());
// Assign the composed soul exactly as from_dir does. let archivist = new( "archivist", Spec { does: "keeps records".into(), soul, model: "m-archivist".into(), ..Default::default() }, ); let model: GenerateFn = Arc::new(|_req: InProcessRequest| { Box::pin(async { Ok(InProcessResponse { content: "archived".into(), ..Default::default() }) }) }); let (r, _rt) = archivist .run(Options { in_process: Some(model), ..Default::default() }, "file this") .await; assert_eq!((r.status.as_str(), r.text.as_str()), ("done", "archived"));
std::fs::remove_dir_all(&dir)?; println!("ok: soul carries {} sections and truncates oversized files safely", found.len()); Ok(())}Fields and behavior
Section titled “Fields and behavior”| Aspect | Behavior |
|---|---|
BOOTSTRAP_ORDER |
AGENTS.md, SOUL.md, IDENTITY.md, USER.md, TOOLS.md, HEARTBEAT.md, MEMORY.md — fixed, identity-first, memory-last. |
| Absent files | Skipped silently — no error, no placeholder section. |
| Per-file cap | 2 MB (2097152 bytes), byte-based (a split multibyte character is decoded lossily). Larger ⇒ truncated + "\n[truncated: exceeds 2 MB bootstrap cap]" appended; on-disk file is never modified. |
| Section format | Each present file becomes "## <filename>\n\n<trimmed body>", joined with "\n\n". |
| found (second tuple element) | The ordered list of filenames that were actually present — use it to know what shaped the soul. |
| Timing | Composition happens once, at session start — the soul is a frozen snapshot for the run (the cache-stability rule). |
See also
Section titled “See also”from_dir— Point at an agent home directory and get a configured agent back.memory_tool— The opt-in built-in that lets a persona write durable notes to its own home.agents::new— the constructorSpec.soulfeeds into.