Agent home — the persona surface (SPEC.md §7E).
A persona is an identity that lives in FILES, keeps durable MEMORY it can edit,
and runs on a HEARTBEAT so it can act unprompted. All three ride seams that
already ship: the composed soul is just a system prompt, the memory tool is a
plain Tool, and the heartbeat is post + wake on the §7D runtime's
INJECTABLE CLOCK. This layer adds no runtime behavior — it is a directory
convention plus composition, not new machinery.
(def ava (home/from-dir "./personas/ava"))
;; one-shot, or as a tool in someone else's toolkit
(def rt (rt/create-runtime {:registry {(:name ava) ava} :llm llm}))
(rt/run-agent rt (:name ava) "what is on my plate?")
(rt/agent-tool rt (:name ava))
;; …or give it its own clock
(def started (home/start-agent ava {:llm llm} {:every-ms 1800000
:on-beat println}))
((:stop started))
Everything here is also usable against a plain client with no runtime at all —
a composed soul is a :system-prompt, and the memory tool is a tool like any
other.
Agent home — the persona surface (SPEC.md §7E).
A persona is an identity that lives in FILES, keeps durable MEMORY it can edit,
and runs on a HEARTBEAT so it can act unprompted. All three ride seams that
already ship: the composed soul is just a system prompt, the memory tool is a
plain `Tool`, and the heartbeat is `post` + `wake` on the §7D runtime's
INJECTABLE CLOCK. This layer adds no runtime behavior — it is a directory
convention plus composition, not new machinery.
(def ava (home/from-dir "./personas/ava"))
;; one-shot, or as a tool in someone else's toolkit
(def rt (rt/create-runtime {:registry {(:name ava) ava} :llm llm}))
(rt/run-agent rt (:name ava) "what is on my plate?")
(rt/agent-tool rt (:name ava))
;; …or give it its own clock
(def started (home/start-agent ava {:llm llm} {:every-ms 1800000
:on-beat println}))
((:stop started))
Everything here is also usable against a plain client with no runtime at all —
a composed soul is a `:system-prompt`, and the memory tool is a tool like any
other.(apply-index-cap content {:keys [max-lines max-bytes]} notes?)The §7E index-cap algorithm over MEMORY.md's (already 2 MB-capped) content. Within both bounds ⇒ unchanged; else keep whole leading lines (a lone over-long first line is cut at a code-point boundary) + the notice.
The §7E index-cap algorithm over MEMORY.md's (already 2 MB-capped) content. Within both bounds ⇒ unchanged; else keep whole leading lines (a lone over-long first line is cut at a code-point boundary) + the notice.
Bootstrap discovery order (§7E): identity first, durable memory last. Each
present file is injected into the soul as a ## <filename> section, in exactly
this order. The order is part of the cross-port contract — a persona whose
MEMORY.md outranked its SOUL.md would behave differently in seven languages.
Bootstrap discovery order (§7E): identity first, durable memory last. Each present file is injected into the soul as a `## <filename>` section, in exactly this order. The order is part of the cross-port contract — a persona whose MEMORY.md outranked its SOUL.md would behave differently in seven languages.
(compose-soul dir)(compose-soul dir opts)Compose the bootstrap files present in dir into one soul string — the frozen
snapshot injected as the system prompt for a whole run.
Returns {:soul "…" :found ["SOUL.md" …]}. Only present files appear, always
in bootstrap-order, each as a ## <filename> section with its body trimmed.
Composition happens once, at session start: the soul is fixed for the run, which
is what keeps a long-lived persona cache-stable.
Options (§7E "Memory hierarchy", both opt-in — absent ⇒ byte-identical):
:memory-index-cap true (200 lines / 25600 bytes) or {:max-lines :max-bytes}; caps MEMORY.md only, after the 2 MB cap, before the trim :notes notes tier on — selects the notice wording
Compose the bootstrap files present in `dir` into one soul string — the frozen
snapshot injected as the system prompt for a whole run.
Returns `{:soul "…" :found ["SOUL.md" …]}`. Only present files appear, always
in `bootstrap-order`, each as a `## <filename>` section with its body trimmed.
Composition happens once, at session start: the soul is fixed for the run, which
is what keeps a long-lived persona cache-stable.
Options (§7E "Memory hierarchy", both opt-in — absent ⇒ byte-identical):
:memory-index-cap true (200 lines / 25600 bytes) or {:max-lines :max-bytes};
caps MEMORY.md only, after the 2 MB cap, before the trim
:notes notes tier on — selects the notice wordingWhat a dream pass is FOR and the order it works in (§7E "Dreaming").
Byte-identical in every port. It becomes the dreamer's ## HEARTBEAT.md
section, so the ordinary heartbeat-prompt wake drives a pass and
HEARTBEAT_OK keeps an idle pass silent.
What a dream pass is FOR and the order it works in (§7E "Dreaming"). Byte-identical in every port. It becomes the dreamer's `## HEARTBEAT.md` section, so the ordinary `heartbeat-prompt` wake drives a pass and `HEARTBEAT_OK` keeps an idle pass silent.
(dreamer dir opts)The built-in dreamer preset (§7E "Dreaming"): a plain AgentDef whose tools
are EXACTLY memory (notes on, over dir) and transcript_search (over
:store) — never the persona's own tools. Its soul is one ## HEARTBEAT.md
section: dream-prompt, then <dir>/DREAM.md (trimmed, if non-empty), then
:instructions (trimmed, if non-empty), joined by blank lines.
Options: :store (required; must carry :list), :instructions, :name
(default <basename(dir)>-dreamer), :model (default "inherit").
Run one pass with (rt/run-agent rt name heartbeat-prompt) or schedule it
with start-agent.
The built-in dreamer preset (§7E "Dreaming"): a plain AgentDef whose tools are EXACTLY `memory` (notes on, over `dir`) and `transcript_search` (over `:store`) — never the persona's own tools. Its soul is one `## HEARTBEAT.md` section: `dream-prompt`, then `<dir>/DREAM.md` (trimmed, if non-empty), then `:instructions` (trimmed, if non-empty), joined by blank lines. Options: `:store` (required; must carry `:list`), `:instructions`, `:name` (default `<basename(dir)>-dreamer`), `:model` (default `"inherit"`). Run one pass with `(rt/run-agent rt name heartbeat-prompt)` or schedule it with `start-agent`.
(from-dir dir)(from-dir dir {:keys [does model tools memory notes dream] :as opts})Build a persona AgentDef from a home directory (§7E).
Discovers the bootstrap files in dir, composes them into a FROZEN soul
snapshot (once, here — the soul is fixed for the whole run, which is what keeps
a long-lived persona cache-stable), and wires a memory tool over the same
directory unless :memory false asks for a read-only persona.
Options:
:name the agent name; default the directory's last segment
:does the routing description a delegating model sees
:model default "inherit" — the runtime's own :llm model
:tools extra tools, placed BEFORE the memory tool
:memory false omits the memory tool
:memory-index-cap cap the injected MEMORY.md (true ⇒ 200 lines / 25600
bytes, or {:max-lines :max-bytes}); off by default
:notes notes tier of the memory tool (note:<slug>, read, list)
:dream {:every-ms :store :instructions} — when this persona is
started with start-agent, a dreamer over the same dir
runs on the same runtime every :every-ms
The return value is a plain AgentDef map, which is this port's spelling of
§7D's Level-1 agent: put it in a runtime's :registry and both directions of
the axiom work on it unchanged — (rt/run-agent rt name prompt) is .run, and
(rt/agent-tool rt name) is .asTool. There is no Agent object to learn,
because a def IS the agent here. run/agent-tool never dream.
Build a persona AgentDef from a home directory (§7E).
Discovers the bootstrap files in `dir`, composes them into a FROZEN soul
snapshot (once, here — the soul is fixed for the whole run, which is what keeps
a long-lived persona cache-stable), and wires a `memory` tool over the same
directory unless `:memory false` asks for a read-only persona.
Options:
:name the agent name; default the directory's last segment
:does the routing description a delegating model sees
:model default `"inherit"` — the runtime's own `:llm` model
:tools extra tools, placed BEFORE the memory tool
:memory `false` omits the memory tool
:memory-index-cap cap the injected MEMORY.md (`true` ⇒ 200 lines / 25600
bytes, or `{:max-lines :max-bytes}`); off by default
:notes notes tier of the memory tool (`note:<slug>`, read, list)
:dream `{:every-ms :store :instructions}` — when this persona is
started with `start-agent`, a `dreamer` over the same dir
runs on the same runtime every `:every-ms`
The return value is a plain AgentDef map, which is this port's spelling of
§7D's Level-1 agent: put it in a runtime's `:registry` and both directions of
the axiom work on it unchanged — `(rt/run-agent rt name prompt)` is `.run`, and
`(rt/agent-tool rt name)` is `.asTool`. There is no Agent object to learn,
because a def IS the agent here. `run`/`agent-tool` never dream.The silent-no-op sentinel: a heartbeat reply containing this surfaces nothing.
The silent-no-op sentinel: a heartbeat reply containing this surfaces nothing.
The prompt a heartbeat wakes the persona with. It contains HEARTBEAT_OK so
the model knows the silent-reply contract, and the word "Heartbeat" so a
HEARTBEAT.md that keys off it can recognise the trigger.
The prompt a heartbeat wakes the persona with. It contains `HEARTBEAT_OK` so the model knows the silent-reply contract, and the word "Heartbeat" so a HEARTBEAT.md that keys off it can recognise the trigger.
Per-file bootstrap cap (§7E), measured in BYTES (2 MiB), not characters. A larger file is injected truncated; the file on disk is untouched.
Per-file bootstrap cap (§7E), measured in BYTES (2 MiB), not characters. A larger file is injected truncated; the file on disk is untouched.
:memory-index-cap true ⇒ this preset: 200 lines / 25600 bytes.
`:memory-index-cap true` ⇒ this preset: 200 lines / 25600 bytes.
(memory-index-notice max-lines max-bytes notes?)The exact notice appended to a capped MEMORY.md (§7E). <N>/<B> are the
CAP values, not the kept counts.
The exact notice appended to a capped MEMORY.md (§7E). `<N>`/`<B>` are the CAP values, not the kept counts.
The notes-on description (§7E, exact): says what memory is FOR.
The notes-on description (§7E, exact): says what memory is FOR.
(memory-tool dir)(memory-tool dir {:keys [notes]})The memory builtin (§7E) — file-backed and OPT-IN. Not one of the default
§4A builtins: it exists only when a home directory is wired.
One tool, three actions over MEMORY.md (the agent's own notes) and USER.md
(its model of the user):
add append an entry
replace swap the FIRST occurrence of an existing substring for with
remove delete the FIRST occurrence of an existing substring
With {:notes true} it also takes target "note:<slug>" (<dir>/memory/<slug>.md,
never injected into the soul) and two read actions, read and list. Without
it, the tool is byte-identical to the pre-hierarchy tool.
Every action writes to DISK. It does NOT touch the live session's system
prompt: under the frozen-snapshot rule the edit loads at the START of the next
session. A replace/remove whose substring is absent is a loud isError.
Writes over one path are serialized within this process (see with-path-lock).
The `memory` builtin (§7E) — file-backed and OPT-IN. Not one of the default
§4A builtins: it exists only when a home directory is wired.
One tool, three actions over `MEMORY.md` (the agent's own notes) and `USER.md`
(its model of the user):
add append an entry
replace swap the FIRST occurrence of an existing substring for `with`
remove delete the FIRST occurrence of an existing substring
With `{:notes true}` it also takes `target "note:<slug>"` (`<dir>/memory/<slug>.md`,
never injected into the soul) and two read actions, `read` and `list`. Without
it, the tool is byte-identical to the pre-hierarchy tool.
Every action writes to DISK. It does NOT touch the live session's system
prompt: under the frozen-snapshot rule the edit loads at the START of the next
session. A `replace`/`remove` whose substring is absent is a loud `isError`.
Writes over one path are serialized within this process (see `with-path-lock`).(start-agent agent-def run-opts {:keys [every-ms on-beat]})Give a persona its own clock (§7E).
On each :every-ms interval the persona posts a tick to its OWN inbox — the
unsolicited rail, where timer ticks COALESCE, so a beat slower than the turn it
starts can never pile up — and, WHEN IDLE, wakes it with heartbeat-prompt.
A reply containing heartbeat-ok is SILENT: only a substantive reply is
collected and handed to :on-beat. Silence is the default, which is the whole
point — a persona that reported every beat would be a cron job with a bill.
Every timer goes through the RUNTIME'S INJECTABLE CLOCK, never a sleep: pass
{:clock (rt/virtual-clock)} in run-opts and a fixture drives the beats with
((:advance! clock) ms), deterministically.
run-opts is anything rt/create-runtime takes except :registry, which is
derived from agent-def (plus any registry the caller supplies, so a persona
with a :team still resolves its team-mates).
A persona built by from-dir with :dream also gets its dreamer registered
on the SAME runtime, spawned under its root, on its own heartbeat at the
dream's :every-ms. Dreamer replies never reach :on-beat / :beats — what
a dream did is in memory/log.md. :stop stops both.
Returns:
:runtime the live runtime — the host's seam for INBOUND channels. §7E is
explicit that channels are the host's job: deliver an external
event by calling rt/post / rt/wake on :handle
:handle the persona's handle id
:beats an atom holding the substantive beats so far (HEARTBEAT_OK
excluded)
:stop (fn []) — cancel the heartbeat(s) and close the tree gracefully
Throws only if the persona cannot be spawned at all; a spawn failure is a configuration error at the root, which is the one place §7D permits a throw.
Give a persona its own clock (§7E).
On each `:every-ms` interval the persona `post`s a tick to its OWN inbox — the
unsolicited rail, where timer ticks COALESCE, so a beat slower than the turn it
starts can never pile up — and, WHEN IDLE, `wake`s it with `heartbeat-prompt`.
A reply containing `heartbeat-ok` is SILENT: only a substantive reply is
collected and handed to `:on-beat`. Silence is the default, which is the whole
point — a persona that reported every beat would be a cron job with a bill.
Every timer goes through the RUNTIME'S INJECTABLE CLOCK, never a sleep: pass
`{:clock (rt/virtual-clock)}` in `run-opts` and a fixture drives the beats with
`((:advance! clock) ms)`, deterministically.
`run-opts` is anything `rt/create-runtime` takes except `:registry`, which is
derived from `agent-def` (plus any registry the caller supplies, so a persona
with a `:team` still resolves its team-mates).
A persona built by `from-dir` with `:dream` also gets its dreamer registered
on the SAME runtime, spawned under its root, on its own heartbeat at the
dream's `:every-ms`. Dreamer replies never reach `:on-beat` / `:beats` — what
a dream did is in memory/log.md. `:stop` stops both.
Returns:
:runtime the live runtime — the host's seam for INBOUND channels. §7E is
explicit that channels are the host's job: deliver an external
event by calling `rt/post` / `rt/wake` on `:handle`
:handle the persona's handle id
:beats an atom holding the substantive beats so far (HEARTBEAT_OK
excluded)
:stop `(fn [])` — cancel the heartbeat(s) and close the tree gracefully
Throws only if the persona cannot be spawned at all; a spawn failure is a
configuration error at the root, which is the one place §7D permits a throw.Notice appended to a bootstrap file truncated at max-file-bytes.
Notice appended to a bootstrap file truncated at `max-file-bytes`.
(valid-note-slug? slug)§7E: ^[a-z0-9][a-z0-9._-]{0,63}$ and no .. — no separators, no traversal.
§7E: `^[a-z0-9][a-z0-9._-]{0,63}$` and no `..` — no separators, no traversal.
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |