Behavior lifecycle
Source:
harness/docs/plugins/lifecycle.md
Status: Current.
Behavior lifecycle
Section titled “Behavior lifecycle”How a selected behavior moves from registration to a turn. The forms are in
polish.md. The design notes are
plugins/hooks-notify-vs-transform-2026-09-20.md §3.
1. Two ambients
Section titled “1. Two ambients”Register. The behavior capability loads the pinned source once for that agent and
configuration. It is an Arrival EnvCapability.
define/tool, guard/*, hook/*, define/reducer, define/method, subscribe,
require, and validate/configuration are macros and record declarations.
toJS turns each handler lambda into a host function; a callback passed into that
function is reverse Rosetta. Lambdas are built and not applied. Nothing is written to a
process-global hook table. A second agent loading the same file gets another
registry. An installed program no bot selected registers nothing. The session
daemon stores the pin and joins notes with loopEnd. It does not call
loadProgram or dispatchBoundary.
Callback. Tool dispatch, turn and round boundaries, and call/method apply
those lambdas. Register forms in a callback throw. Reducer application is a
third, pure ambient: proposals, method calls, and notifications are absent.
Missing pinned source is an error. The host does not substitute a newer file. A rendered prompt cannot select a different program than the one the host resolved.
2. Pin and re-attunement
Section titled “2. Pin and re-attunement”Named bots, .prompt one-shots, and the foreground agent use the same rule.
An existing pin wins. A new instance takes the authored selection. A child
takes its own selection.
Re-attunement happens at a turn boundary. An active turn keeps the handlers it started with. Outstanding background work keeps the binding it started with. The next idle turn takes the new pin. Replay uses the reducer revision recorded on the action. A revision change without a migration or reset fails. Opening a shared owner that already exists does not replace its ABI. A bot update cannot silently upgrade that owner for other callers.
3. Turn and round
Section titled “3. Turn and round”guard/turn and guard/round wrap the boundary. hook/turn and hook/round
run at the same boundary, in parallel, after the guard chain allows it. A
refusal runs :post-fail and does not continue. A throw from the core does the
same. Round-end hooks finish before the continuation decision. Turn-end hooks
run after the last round.
At round start, ambient and activating notes enrich the upcoming call and do not reserve another round. At round end, activating notes OR with the host’s own continuation. Ambient notes never do. The tasks notebook uses that split: an open page activates, a pending-only notebook is ambient, and an empty notebook is silent. Each round emits a new note. Closing the open page stops activation on the next check. Old notes are not withdrawn. They are consumed when placed, or they expire at turn seal.
Turn and round hooks receive the boundary record passed into dispatch.
A branch change is ambient context. The same branch is ambient again only when the context is fresh after compaction. The same branch in the same context is quiet.
admit/read records a delivery. Replay returns the recorded payload and does
not call the host again. Hydration adopts recorded notes and consumption. It
does not emit them a second time, and consuming an already consumed note throws.
Spawning a worker does not copy the caller’s registrations and does not drop the caller’s lineage. Examiner, worker, and foreground sessions dispatch only the hooks in their own registry.
4. Tool chain
Section titled “4. Tool chain”For one agent and one call:
- If the effect is outside the bot’s ceiling, stop. The floor does not run.
- If no guard and no hook matches the path, stop. Scheme does not run.
- Guards run in order on the raw value. Each
invokeis single-use. - The floor sees the unwrapped value and every mark still present.
:prehooks run on that filtered value.- The core runs once.
:posthooks may append diagnostics beside the core result. They do not replace it.:post-failruns when a guard refuses or the chain throws. If the floor never saw the call, the hook receives a hidden refusal rather than the raw argument.
Depth 0 rejects re-entry into a registration that is already on the lineage.
:reentrant-depth N allows N extra entries. Sibling hooks have their own ids.
There is no process-wide busy flag. Two method callers commit independently.
A conflict is a stale revision or a reused call id, not a lock.
Retry is guard-only. It re-enters at the raw value. It does not resume in the middle of the chain. An exhausted budget is a refusal.
5. Method admission
Section titled “5. Method admission”call/method resolves the pinned receiver, checks the grant and the ABI,
validates input, runs the handler on the fold of the observed revision, and
validates output. Queries record the result and do not append. Commands append
proposals only when the owner revision is still the observed one and no
protected action lacks host authority.
Caller replay returns the stored result. It does not read a newer fold and it
does not append. Nested call/method uses a child call id and the same lineage
budget.
6. What the exemplars do
Section titled “6. What the exemplars do”Tasks. Session methods incomplete, create, start, stamp, drop,
after, and validate. create refuses a validation program that does not
look at the tree and contain tasks/write-completion-verdict. The foreground
round hook reads incomplete: "open" is activating, "pending" is ambient,
and #f is silence. stamp and limbo are host-sealed. Project accept /
complete is a second owner: acceptance is not completion, the id is stable,
and a session rollback leaves the project row. The JS module remains the host
projection (@inhuman.tools/plugin-tasks). plugin.json does not list plugins[].
Offload / tingle. One project reducer. The global module is the same file,
a second owner. The writer may put-memory. The scout may get-memory and
search. A missing citation is a refusal. The same request id and input do not
append twice. A different input on that id is an error. A stale revision does
not overwrite. Search returns #f when the body is longer than 800 characters
or does not contain the query. A write does not notify. SQLite and the private
MCP process are an adapter on the JS default export. plugin.json does not
select that module.
Goal. Session fields text, status, and loops. The round hook ticks
and activates while the goal is active and loops remain. :post-fail sets
blocked. The goal dies with the session. A child does not inherit the
parent’s hooks. /goal is the pack file
tools/inhuman/commands/goal.scm, not a kernel default.
Fusion. Session fields text, status, steps, and recall. At round
end the hook reads the plan and a granted offload search, advances one step,
and emits one activating note. A miss notifies the plan text. A hit notifies
the plan and the fact, and stores the recall. An empty plan is silent.
:post-fail sets blocked and does not tick. The planner does not subscribe
and does not stand on a tool argument. put-memory is not granted. The plan
dies with the session. Project memory survives rollback. /plan is the pack
command file, the same shape as /goal.
7. Removal that has not happened
Section titled “7. Removal that has not happened”InhumanPlugin stays a host-internal entity. Host builtins construct it, and
applyPackPlugins compiles an already built module into the catalog. The pack
loader does not import plugins[]. PluginProjection.loopEnd still joins host
addenda with behavior notes. They do not replace those addenda. Dropping the bag
is a later change, after those builtins have a caller on this runtime.