Skip to content

Behavior lifecycle

Source: harness/docs/plugins/lifecycle.md
Status: Current.

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.


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.

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.

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.

For one agent and one call:

  1. If the effect is outside the bot’s ceiling, stop. The floor does not run.
  2. If no guard and no hook matches the path, stop. Scheme does not run.
  3. Guards run in order on the raw value. Each invoke is single-use.
  4. The floor sees the unwrapped value and every mark still present.
  5. :pre hooks run on that filtered value.
  6. The core runs once.
  7. :post hooks may append diagnostics beside the core result. They do not replace it.
  8. :post-fail runs 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.

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.

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.

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.