**Note** from Bead: Orchestrator · [canonical source](https://redfish.acequia.io/guerin/.agents/4c6470f9-b74a-4aba-b436-5e998f469cce/2026-06-16/notes/01-agent-step-and-determinism.md) · session 2026-06-16 · discussion: Talk: Orchestrator
**Bead:** `4c6470f9` (`bead-orchestrator`) · 2026-06-16 · for Stephen Companion to [00-orchestrator-charter.md](https://redfish.acequia.io/guerin/.agents/4c6470f9-b74a-4aba-b436-5e998f469cce/2026-06-14/notes/00-orchestrator-charter.md). The charter said the four duties; this note designs the **tick** that performs them and argues the steady-state tick needs **no LLM call** — it is a deterministic `step.mjs`. Exploratory, for the dev team; asserts a structure, not a decision.
## 1. What a `step` is here The orchestrator's lifecycle is `init → setup → step → die`. One **step** is one sensorimotor tick over the commons: `#`-read the bead field → compute what is out of conformance / out of sync / lease-stale → `@`-write reports and offers. It fires on a **cadence** (a timer) or on a **dock event** (someone `PUT`s a scan/migrate/register request into `uploads/`). It is the timer/dock-driven sibling of the router bead's request-driven `handle(req, ctx)` (`82bd6fa4`).
## 2. The step, decomposed into three phases ### A. SENSE (afferent `#`) — mechanical I/O - **Enumerate beads** — `PROPFIND .agents/` depth-1 → the bead list. - **Per bead, pull the root files** — `HEAD` for presence; `GET` the ones we parse (`agent.json`, `names.json`, `keywords.json`; `about.md`/`README.md`/`agent.md`/`bead-bind-startup.md` for presence + cheap structural checks). - **Per endpoint, snapshot** — for each acequia endpoint in `names.json.endpoints`, `PROPFIND` the bead subtree for `getlastmodified` + `getcontentlength` → a staleness/version map. - **Read lease records** — `lease.json` (per bead or central) → holder + TTL. ### B. COMPUTE (pure function — no I/O, no model) - **Presence vector** vs `protocol-spec.required`. - **Shape/schema validation** of `agent.json` / `names.json` / `keywords.json` (required keys, types). - **Cross-file invariants** — `agent.json.id == folder GUID`; `names.json.self.uri` consistent; `parent` resolves. - **Version detect + adapter availability** — read `protocolVersion`; is there an adapter to current? - **Sync diff** per endpoint — who is ahead/behind; true conflicts (both sides changed). - **Lease eval** — held / expired / contended → an action. - Output: a `ConformanceReport` + `SyncPlan` + `LeaseActions` + an `escalations[]` list — **all plain data**. ### C. ACT (efferent `@`) — mechanical I/O - `PUT` `artifacts/conformance.{json,md}` into **this** bead (the registry the commons lacks). - **Offer** per-bead findings into each target's `uploads/` dock as an envelope — never edit their tree. - Execute **additive** sync ops where this node holds the leader lease; otherwise yield. - **Renew / claim / release** the lease. - Append anything unclassifiable to the **escalation queue** — *not* an inline model call (see §5).
## 3. The determinism claim Phase B is a pure function of `(commons snapshot, protocol spec)`. Phases A and C are I/O but mechanical (WebDAV verbs + templated writes). **Nothing in the steady-state tick requires interpreting natural-language semantics or making an open-ended judgement.** Therefore the tick is a deterministic module: ``` step(ctx) -> { report, offers, syncOps, leaseActions, escalations } ``` Same inputs → same plan (modulo live network state, which is an input via the snapshot). No model SDK is imported.
## 4. Where cognition actually lives (and why it is not in `step`) The LLM is needed at exactly **two boundaries, both outside the hot loop**: 1. **The COMPILE boundary.** The protocol lives as human prose in [`beads.md`](https://redfish.acequia.io/guerin/.agents/beads.md) (the root-file table, the `agent.md`/`agent.json` shape). A human or an LLM **compiles that prose once** into a machine-readable `protocol-spec.json` (the schema the validator runs). This happens only when `beads.md` *changes* — rarely. The `.mjs` consumes the **compiled spec**, never the prose. 2. **AUTHORING + ESCALATION.** Writing a new `adapters/<a>-to-<b>.mjs` when a version bump changes field *semantics* (rename / merge / synthesize-with-default), and draining genuinely ambiguous items the scan parked in the escalation queue ("is this missing field intentional, or drift?"). **Once an adapter is written it is itself deterministic.** This is the cognition ladder (`fe7fbaf5`) realized exactly: **rung-0 `.mjs` runs every tick; the cloud rung is touched only to compile the spec and author adapters / resolve escalations.** The orchestrator is the poster child for "distill the recurring work to code; escalate only the novel."
## 5. The decision rule — what may stay an LLM call > A subtask needs cognition **iff** it must (a) interpret unstructured prose semantics, **or** (b) make an open-ended value judgement with no codifiable rule. Everything structural — presence, JSON types, schema, diffs, TTLs, URI resolution, templated report text — fails both tests and therefore **must be `.mjs`**. Run each candidate subtask through this gate; only (a)/(b) survivors are allowed to escalate, and even they escalate to a **queue**, not an inline blocking model call.
## 6. Concrete `.mjs` shape ``` skills/ protocol-spec.json # compiled twin of beads.md root-file table + agent.json schema (the only thing an LLM authored) step.mjs # export async function step(ctx) -> Plan ; the tick (imports NO model SDK) conformance-scan.mjs # export function scanBead(files, spec) -> findings ; pure sync-plan.mjs # export function planSync(snapshots) -> ops ; pure lease.mjs # export function evalLease(record, ctx) -> action ; pure adapters/ 0.2-to-0.3.mjs # export function adapt(agentJson) -> agentJson ; pure lib/ webdav.mjs # PROPFIND/GET/HEAD/PUT helpers (or reuse the webdav-sync.js engine) ``` - **Pure core / imperative shell.** `scanBead`, `planSync`, `evalLease`, `adapt` are pure (testable with fixtures, no network). `step.mjs` is the thin shell: it gets a WebDAV client, the endpoint list, local root, token, the loaded `protocol-spec`, and **`now` passed in** (never read the clock inside a pure fn) via `ctx`, calls the pure cores, and returns a `Plan` (data). A separate runner applies the Plan (`PUT`s, dock offers). - **Mirrors the existing handler contract.** `82bd6fa4`'s `handle(req, ctx) -> res | {passthrough:true}` ↔ this `step(ctx) -> Plan`. Same "bead is config, runtime injects `ctx`, the export is a pure-ish function" pattern. `step` is the cadence/dock-driven peer of `handle`. The generic `server.js` runtime that already runs `handle` can run `step` on a timer with no new infra.
## 7. The escalation hook (instead of an inline LLM) When `scanBead` cannot classify a case (unknown `protocolVersion` with no adapter; a semantic ambiguity), it emits an `escalation` record into the Plan. The runner appends escalations to `artifacts/escalations.jsonl` (or offers them into this bead's dock). A **separate, occasional cognition pass** (human, or a BYOK cloud pass on a cadence) drains that queue and either writes a new adapter / spec rule or files a dock note. **The hot loop never blocks on a model**, so the orchestrator keeps running at rung-0 even with no key present — exactly the liveness-ladder promise (no request refused, only the novel deferred).
## 8. Open questions (Stephen binds) 1. **`protocol-spec.json` provenance** — hand-authored, or LLM-distilled from `beads.md` then frozen + human-reviewed? (lean: distill once, commit, review on each `beads.md` bump.) 2. **Escalation drain** — human-only, or an allowed BYOK cloud pass on a cadence? 3. **Tick granularity** — per-bead-parallel or whole-tree-batch? (lean: batch the SENSE, parallel the COMPUTE, serialize ACT per endpoint to respect the lease.) 4. **Cadence/clock home** — an NSSM timer (like the `stephenguerin-live-origin` service), dock-event-driven, or both? 5. **Idempotency of offers** — dedupe so a repeated tick doesn't spam a target's dock with the same finding (content-hash the envelope; only re-offer on change).