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.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 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.
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.(from-dir dir)(from-dir dir {:keys [does model tools memory] :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
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.
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 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.
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-tool dir)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 an existing substring for with
remove delete an existing substring
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, which is what keeps a long-lived persona cache-stable — and the tool's
own description says so, because the model is the one that has to know. A
replace/remove whose substring is absent is a loud isError, never a
silent no-op.
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 an existing substring for `with` remove delete an existing substring 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, which is what keeps a long-lived persona cache-stable — and the tool's own description says so, because the model is the one that has to know. A `replace`/`remove` whose substring is absent is a loud `isError`, never a silent no-op.
(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).
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 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).
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 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`.
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 |