Skip to content

Toolnexus.Agents.Home.compose_soul

Elixir · package toolnexus · SPEC §7E · elixir/lib/toolnexus/agents/home.ex

@spec compose_soul(String.t()) :: {String.t(), [String.t()]}
def compose_soul(dir)
# bootstrap order: AGENTS.md, SOUL.md, IDENTITY.md, USER.md, TOOLS.md, HEARTBEAT.md, MEMORY.md
# (Toolnexus.Agents.Home.bootstrap_order/0)

Composes whichever bootstrap files exist under dir, in the canonical order, into one soul string — the persona’s system prompt. Each present file becomes a ## <file> section; absent files are silently skipped; a file over 2 MB is truncated with a notice (the file on disk is untouched). Returns {soul, found}, where found lists the discovered filenames in the order they were composed.

  • The directory IS the agent — a persona’s identity lives in files (SOUL.md, IDENTITY.md, …) that a human or another process edits directly, and you want that editable state to become a system prompt without hand-rolling the file walk and ordering.
  • You need the composition step in isolation — to preview a soul before running the agent, to snapshot it for a log, or to build something other than an AgentDef from the same files.
  • You’re auditing what a persona will seefound tells you exactly which files were present at composition time.

Composition happens once, at session start — a frozen snapshot. A later memory tool write changes the files on disk, but not the live prompt already in context; the next session’s compose_soul/1 call is what picks it up. This is deliberate (cache-stable prompts), not a bug.

1. The smallest useful call — one bootstrap file

Section titled “1. The smallest useful call — one bootstrap file”
alias Toolnexus.Agents.Home
dir = Path.join(System.tmp_dir!(), "toolnexus_doc_compose_soul_1_#{System.unique_integer([:positive])}")
File.rm_rf!(dir)
File.mkdir_p!(dir)
File.write!(Path.join(dir, "SOUL.md"), "You are Kestrel, a calm and precise research agent.")
{soul, found} = Home.compose_soul(dir)
true = found == ["SOUL.md"]
true = soul == "## SOUL.md\n\nYou are Kestrel, a calm and precise research agent."
IO.puts("ok: composed #{byte_size(soul)} bytes from #{Enum.join(found, ", ")}")

2. The realistic case — several files, canonical order, missing files skipped

Section titled “2. The realistic case — several files, canonical order, missing files skipped”

Files are composed in the bootstrap order, not directory-listing order, and a file that isn’t present is simply absent from found — no error, no placeholder section.

alias Toolnexus.Agents.Home
dir = Path.join(System.tmp_dir!(), "toolnexus_doc_compose_soul_2_#{System.unique_integer([:positive])}")
File.rm_rf!(dir)
File.mkdir_p!(dir)
# Written out of canonical order on purpose — composition order must not depend on write order.
File.write!(Path.join(dir, "MEMORY.md"), "- prefers concise answers")
File.write!(Path.join(dir, "AGENTS.md"), "Follow the repo's CLAUDE.md conventions.")
File.write!(Path.join(dir, "IDENTITY.md"), "Name: Kestrel. Role: research agent.")
# TOOLS.md, USER.md, HEARTBEAT.md, SOUL.md are all absent.
{soul, found} = Home.compose_soul(dir)
true = found == ["AGENTS.md", "IDENTITY.md", "MEMORY.md"]
true = String.starts_with?(soul, "## AGENTS.md")
true = String.contains?(soul, "## IDENTITY.md\n\nName: Kestrel. Role: research agent.")
true = String.ends_with?(soul, "## MEMORY.md\n\n- prefers concise answers")
IO.puts("ok: #{length(found)} bootstrap file(s) composed in canonical order")

3. The full surface — the 2 MB truncation cap

Section titled “3. The full surface — the 2 MB truncation cap”

A file that exceeds the byte cap is truncated in the composed soul, with a notice appended; the file on disk is left exactly as written.

alias Toolnexus.Agents.Home
dir = Path.join(System.tmp_dir!(), "toolnexus_doc_compose_soul_3_#{System.unique_integer([:positive])}")
File.rm_rf!(dir)
File.mkdir_p!(dir)
huge = String.duplicate("x", 2 * 1024 * 1024 + 100)
File.write!(Path.join(dir, "MEMORY.md"), huge)
{soul, found} = Home.compose_soul(dir)
true = found == ["MEMORY.md"]
true = String.contains?(soul, "[truncated: exceeds 2 MB bootstrap cap]")
# the composed section is capped, but the on-disk file is untouched
true = byte_size(File.read!(Path.join(dir, "MEMORY.md"))) == byte_size(huge)
true = byte_size(soul) < byte_size(huge)
IO.puts("ok: oversized MEMORY.md truncated to #{byte_size(soul)} bytes (disk file untouched)")
Field Type What it is
dir (arg) String.t() The persona’s home directory.
soul (return) String.t() The composed system prompt — ## <file> sections, joined with blank lines.
found (return) [String.t()] Discovered bootstrap filenames, in canonical order.