Skip to content

toolnexus.agents.home/compose-soul

Clojure (JVM) + cljgo · package net.clojars.muthuishere/toolnexus · SPEC §7E · clojure/src/toolnexus/agents/home.cljc

(compose-soul "./personas/ava")
;; => {:soul "## SOUL.md\n\nI am Ava\n\n## MEMORY.md\n\n- …"
;; :found ["SOUL.md" "MEMORY.md"]}

The directory is the agent. compose-soul reads the bootstrap files present in dir and returns one string to use as a system prompt, plus the list of files it found. Absent files are skipped entirely — they leave no heading and no empty section.

Identity first, durable memory last. This order is part of the cross-port contract: a persona whose MEMORY.md outranked its SOUL.md would behave differently in seven languages.

# file role
1 AGENTS.md operating instructions
2 SOUL.md identity / voice
3 IDENTITY.md who the agent is
4 USER.md model of the user
5 TOOLS.md tool guidance
6 HEARTBEAT.md what to do on a heartbeat
7 MEMORY.md durable long-term notes

Each present file becomes a ## <filename> section with its body trimmed. Each is read with a 2 MiB cap measured in bytes (2097152), not characters — a larger file is injected truncated with a notice, and the file on disk is untouched.

  • You keep an agent’s identity in files and want it in the system prompt without hand-rolling read-and-concatenate.
  • You want the identity frozen for a run. Composition happens once, at session start; the soul does not shift under a long-lived agent, which is what keeps it cache-stable.

1. Compose a persona and use it as a system prompt

Section titled “1. Compose a persona and use it as a system prompt”
(require '[toolnexus.agents.home :as home]
'[toolnexus.client :as client]
'[clojure.string :as str]
'[koine.fs :as fs])
(def dir (str (fs/temp-dir! "ava")))
(fs/write-file (str dir "/SOUL.md") "I am Ava. I answer in one sentence.")
(fs/write-file (str dir "/MEMORY.md") "- the user prefers Clojure")
(def composed (home/compose-soul dir))
(println "found:" (pr-str (:found composed)))
(println (:soul composed))
;; Bootstrap ORDER, not directory order: SOUL.md outranks MEMORY.md.
(assert (= ["SOUL.md" "MEMORY.md"] (:found composed)))
(assert (< (str/index-of (:soul composed) "## SOUL.md")
(str/index-of (:soul composed) "## MEMORY.md")))
;; It is just a string — hand it to any client.
(def c (client/create-client {:base-url "http://127.0.0.1:1"
:style "openai"
:model "gpt-4o-mini"
:api-key "not-used"
:system-prompt (:soul composed)}))
(assert (str/includes? (:system-prompt c) "I am Ava"))
(fs/delete-tree! dir)
(println "OK")

A persona with one file gets exactly one section — not seven empty headings.

(require '[toolnexus.agents.home :as home]
'[koine.fs :as fs])
(def dir (str (fs/temp-dir! "minimal")))
(fs/write-file (str dir "/SOUL.md") " just me ")
(def composed (home/compose-soul dir))
(println (pr-str (:soul composed)))
(assert (= "## SOUL.md\n\njust me" (:soul composed))) ; body trimmed, nothing else
(assert (= ["SOUL.md"] (:found composed)))
;; An empty directory composes an empty string — never nil.
(def empty-dir (str (fs/temp-dir! "empty")))
(assert (= "" (:soul (home/compose-soul empty-dir))))
(assert (= [] (:found (home/compose-soul empty-dir))))
(fs/delete-tree! dir)
(fs/delete-tree! empty-dir)
(println "OK")
(require '[toolnexus.agents.home :as home])
(println (pr-str home/bootstrap-order))
(assert (= ["AGENTS.md" "SOUL.md" "IDENTITY.md" "USER.md"
"TOOLS.md" "HEARTBEAT.md" "MEMORY.md"]
home/bootstrap-order))
(assert (= 2097152 home/max-file-bytes)) ; bytes, not characters
(println "OK")