Replacing the framing I wrote an hour ago. I had the node's own agent sitting outside the lifecycle as an exemption, which is a rule somebody has to remember. Provisioned the ordinary way and constrained is a rule the system enforces, and it is one row like any other rather than a category every query listing agents has to special-case. It also reads the original sentence more carefully. "Exempt from the hiring lifecycle" is exempt from hiring, not from having a lifecycle. Its lifecycle is the node's -- provisioned at enrolment, retired when the node is retired. Same states, a different thing driving them, and no exemption needed. The constraints are now the four nonsense states written as things that cannot happen rather than as an argument: not retirable, reassignable or deletable while its node exists; exactly one per node. And a distinction that was missing -- its existence is immutable, its engram is not. Freezing the personality would remove the way a node is configured. Disabling is the better half of this. A node with no agent is a silence somebody has to diagnose; a node whose agent is disabled answers saying so, immediately, with no model invoked -- the queue is still consumed and the state is the reply. That is the host's own rule about a service that does not exist, applied one tier up: absence must never be indistinguishable from a failure to answer.
151 lines
8.1 KiB
Markdown
151 lines
8.1 KiB
Markdown
---
|
|
topic: the mesh
|
|
status: accepted
|
|
date: 2026-07-12
|
|
deciders: jochen
|
|
reconstructed: true
|
|
---
|
|
|
|
# 3. An agent is a persistent employee, not an instance of a pool
|
|
|
|
> Reconstructed after the fact from the evidence cited below.
|
|
|
|
## Context
|
|
|
|
Agents were originally a **pool**: a named kind of worker, scaled to some number of
|
|
interchangeable instances. Work went to whichever instance was free.
|
|
|
|
That model has no place to put the things that turn out to matter. An agent that accumulates
|
|
knowledge of a domain cannot keep it, because the next task lands on a different instance. An
|
|
agent cannot own a workspace, because there are several of it. It cannot be held to a policy —
|
|
warned for a violation, then dismissed — because there is no continuing subject to warn.
|
|
|
|
Scaling was also solving a problem the mesh does not have. Instances were being multiplied to
|
|
get concurrency, when concurrency is a property of how much work one agent may hold at once.
|
|
|
|
## Considered options
|
|
|
|
1. **Keep the pool, attach memory to the pool.** Rejected: shared memory across
|
|
interchangeable workers is a knowledge base, not an agent's experience, and the mesh
|
|
already has one.
|
|
2. **Keep the pool, make instances sticky.** Rejected as a pool pretending to be identities —
|
|
identity by scheduling accident, lost on any restart.
|
|
3. **One agent is one persistent identity, with concurrency as a property of it.** Chosen.
|
|
|
|
## Decision
|
|
|
|
An agent is a **singular, named, persistent identity**: a home node, a workspace on that node,
|
|
accumulating memory, and a lifecycle — hired, active, draining, retired. Not a pool member.
|
|
|
|
Concurrency is a property of the agent, not a count of copies: an agent has a cap on how many
|
|
sessions it may hold at once.
|
|
|
|
Lifecycle is explicit and has verbs. An agent is hired onto a node; it may be reassigned while
|
|
idle; it is retired by draining first, and forced only deliberately. Retired agents are not
|
|
deleted.
|
|
|
|
Surge capacity is expressed within the model rather than against it: a template agent is a
|
|
blueprint, cloned into a real agent with a lifetime when a queue grows, drained and retired
|
|
when it expires. A temporary employee is still an employee.
|
|
|
|
Some agents are **human**. What differs is modality — how the agent acts — not category. A node
|
|
itself is an agent of a kind exempt from the hiring lifecycle.
|
|
|
|
### The node's own session
|
|
|
|
*Written 2026-08-29. The sentence above is the whole of this and had been left as one line, which
|
|
is why it kept being read as a leftover rather than as the design.*
|
|
|
|
**Every node holds one session of its own, permanently.** It listens on its own queue, anything in
|
|
the mesh may prompt it, and it remembers — what it was asked ten minutes ago and what it was asked
|
|
last week, across every caller, the way any conversation is remembered by both sides. Its system
|
|
prompt is the node's **engram**: the personality that makes one node's answers recognisably its
|
|
own.
|
|
|
|
Nothing about it is request-response. A caller asks, the node answers, the exchange stays.
|
|
|
|
**It is the same mechanism as a hired agent, and deliberately not the same lifecycle.** That
|
|
distinction is the answer to a question asked repeatedly and worth settling here:
|
|
|
|
| the same | different |
|
|
|---|---|
|
|
| a persistent session, accumulating memory, a system prompt, a scoped tool list, addressable by message | how it comes into existence, and whether it can stop |
|
|
|
|
**One implementation, two ways of existing: hired, or inherent to a node.** Building the mechanism
|
|
twice is real duplication and the concern was right; collapsing the lifecycles is the other mistake
|
|
and it is worse.
|
|
|
|
**It is provisioned the ordinary way and made immutable — not held outside the system.** The
|
|
sentence above says *exempt from the hiring lifecycle*, and the precision matters: it is exempt
|
|
from **hiring**, not from having a lifecycle. Its lifecycle is the **node's** — provisioned when
|
|
the node enrols, retired when the node is retired. Same states, a different thing driving them.
|
|
|
|
That distinction is what keeps it inside the model. A thing genuinely held outside would have to be
|
|
special-cased by everything that lists agents; a thing provisioned normally and constrained is one
|
|
row like any other, and the constraints are **checkable** rather than remembered:
|
|
|
|
| | |
|
|
|---|---|
|
|
| **cannot be retired, reassigned, or deleted while its node exists** | it *is* that machine's voice — retiring it leaves a node nothing can talk to, moving it puts one machine's mind on another |
|
|
| **exactly one per node** | with two, nothing decides which replies when the node is addressed; with none, the node is mute |
|
|
| **may be disabled and re-enabled** | ordinary, and see below |
|
|
| **its engram may be changed** | its existence is immutable, its personality is not — that is how a node is configured |
|
|
|
|
[ADR 0001](0001-mesh-brokers-nodes-host-agents-think.md)'s *the two agent rows per node merge* is
|
|
the same fact from the other side: **one per node** — not zero, and not two.
|
|
|
|
**Disabled is a state that answers.** A node prompted while its agent is disabled replies saying
|
|
so, immediately, without a model being invoked. It does not time out and it is not silence — the
|
|
queue is still consumed, and the answer is the state.
|
|
|
|
That is the whole reason disabling is better than not provisioning. A node with no agent is a
|
|
silence somebody has to diagnose; a node whose agent is disabled tells you what is wrong in the
|
|
reply. It is the same rule the host follows about a service that does not exist, applied here:
|
|
**absence must never be indistinguishable from a failure to answer.**
|
|
|
|
### What is scoped, and what is not
|
|
|
|
**Its tool list is its own and narrower than a session a person drives.** The same scoping any
|
|
agent has; a different list.
|
|
|
|
**There is no authorisation between nodes.** Every node is the operator's own, and a prompt from
|
|
one is a prompt from the operator. Asking a node something is asking a colleague, and colleagues do
|
|
not present credentials.
|
|
|
|
Stated once so it is not discovered later: **the mesh boundary is therefore the security
|
|
boundary.** Anything inside can reach whatever any node can reach, which is what makes the token
|
|
and the overlay the entire perimeter ([ADR 0004](0004-a-node-and-how-it-joins.md),
|
|
[ADR 0007](0007-connectivity.md)).
|
|
|
|
**How a node passes a question on is the node's own choice, not a field in a message.** Asked
|
|
something it must ask a third node about, a node may say who is asking or may simply ask — the way
|
|
a person relaying a question decides how to phrase it. That follows from the engram, not from a
|
|
protocol. What it costs is a machine-readable chain of who ultimately asked; what each node was
|
|
asked, and by whom, remains in that node's own record.
|
|
|
|
**A node thinks about one thing at a time**, being one session. Callers queue, and a long answer
|
|
delays the others.
|
|
|
|
## Consequences
|
|
|
|
- Memory, workspace and reputation have a subject to belong to. Policy becomes possible: an
|
|
agent that violates a rule can be warned, and warned agents can be dismissed.
|
|
- The mesh gained a hiring model, and with it the question of who may hire.
|
|
- Scaling by adding instances is gone. If one agent is saturated, either its session cap rises
|
|
or another agent is hired — both deliberate acts.
|
|
- The transition was not free. Lifecycle columns had to reach every query that selects an
|
|
agent, and the ones that were missed failed at the moment of hiring rather than at startup.
|
|
- This is the decision [ADR 0001](0001-mesh-brokers-nodes-host-agents-think.md) generalises:
|
|
one kind of participant, differing only in modality.
|
|
|
|
## References
|
|
|
|
- `docs(adr): agents as persistent employees + MINERVA librarian` (#495), 2026-07-12 — the
|
|
original record, in the code repository.
|
|
- `feat(B4): one persistent employee, N sessions — rename max_instances → max_sessions` (#547)
|
|
and `feat(noxflow): B3 — workspace provisioner for agent employee model` (#549), 2026-07-20.
|
|
- `feat(noxflow): warn-then-fire agents who merge to main without review` (#209), 2026-06-01 —
|
|
policy that presumes a continuing subject, predating the model that provides one.
|
|
- Knowledge base: `agents/employee-lifecycle`, `agents/temp-surge`, `agents/workspace-layout`.
|
|
- The migration cost: `troubleshooting`/`noxflow-agent-enriched-select-missing-lifecycle-columns` (#546).
|