Agent Orchestrator

Framework-level supervisor for every agent blamejs ships (b.mail.agent today; future search-index / AI-classify / DSR / c2pa-watermark agents). The orchestrator owns:

- **Registry** (register / lookup / unregister / list) — pluggable backend; in-memory default, durable via operator- supplied b.config.loadDbBacked for restart-survival. Rows are sealed at rest via b.cryptoField when a vault is configured (the default in a booted app), so tenant names + endpoint metadata don't leak in DB dumps. - **Sharded topics** (spawnConsumers) — consistent-hash route per-shard so each tenant's traffic owns one shard's ordering. - **Leader-elected singletons** (elect) — composes b.cluster DB-row election. Operator marks methods that must run on exactly one node (MDN batch dispatch, virus-DB refresh, journal compaction) as singletons. - **Drain** (drain) — consumer.stop() on every spawned consumer; wait for in-flight envelopes via b.outbox; audit. Wires into b.appShutdown as a registered phase. - **Health probe** (health) — aggregates per-agent + per- consumer + per-election state into one shape for b.middleware.healthcheck.

The orchestrator is the **in-process supervisor of agents**, NOT the **OS-level supervisor of processes**. Spawn / restart-on- crash / autoscaling / network routing all delegate to pm2 / systemd / k8s / Nomad — the framework doesn't compete.

var orch = b.agent.orchestrator.create({
  audit:        b.audit,
  permissions:  myPerms,
  backend:      operatorBackend,    // optional; in-memory default
});

await orch.register("tenant-acme.mail", mailAgent, { agentKind: "mail" });
var agent = await orch.lookup("tenant-acme.mail");

b.agent.orchestrator.create(opts) #

stable0.9.21
{
  audit:        b.audit namespace,            // optional; defaults to b.audit
  permissions:  b.permissions instance,       // optional; orchestrator skips RBAC if absent
  backend:      { get, set, delete, list },   // optional; in-memory default
  cluster:      b.cluster module,             // optional; defaults to b.cluster
  appShutdown:  b.appShutdown.create()        // optional; orchestrator adds an "agent.orchestrator.drain" phase via addPhase() if supplied
}

Create the orchestrator. Returns a singleton-style facade with registry / spawn / elect / drain / health methods. Operator runs one orchestrator per process; multi-process deployments share coordination via the backing store + b.cluster.

var orch = b.agent.orchestrator.create({});
await orch.register("tenant-acme.mail", mailAgent, { agentKind: "mail" });
var agent = await orch.lookup("tenant-acme.mail");
var folders = await agent.folders({ actor: { id: "u1" } });

b.agent.orchestrator.hydrate(name, agent) #

stable0.9.57

Attach an in-process live agent reference to a row that already exists in the persistent registry backend. The canonical boot-phase contract: the *first* process to start a new agent calls register() (writes the backend row + holds the live ref); every *subsequent* process that picks up the row from durable storage (cross-orchestrator-restart, multi-process deploy, k8s pod recreate) calls hydrate(name, agent) to install its local live ref WITHOUT trying to re-write the backend row (which would refuse with agent-orchestrator/duplicate).

Throws agent-orchestrator/not-in-registry when no backend row exists for name. Throws agent-orchestrator/already-hydrated if the live ref is already installed (operator's boot phase ran twice).

Boot-phase contract: 1. Process A calls register("tenant-acme.mail", agent, regOpts) → backend row written; A.liveAgents holds the ref. 2. Process A crashes / redeploys. 3. Process B starts: backend row already exists. 4. Process B walks the registry via list() → sees rows it hasn't hydrated yet. 5. For each, Process B reconstructs the agent locally (from its operator config) and calls hydrate(name, agent). 6. lookup("tenant-acme.mail") from Process B now returns the live ref instead of throwing not-hydrated.

var rows = await orch.list({});
for (var i = 0; i < rows.length; i += 1) {
  var name = rows[i].name;
  var agent = buildAgent(rows[i]);
  await orch.hydrate(name, agent);
}

b.agent.orchestrator.shardFor(shardKey, shards) #

stable0.9.21

Consistent-hash router for sharded topic dispatch. Operator passes a stable shard-key (e.g. tenantId or actor.id); orchestrator picks the topic suffix so each tenant's traffic owns one shard's ordering. Uses FNV-1a 32-bit — fast, good distribution for short keys, no cryptographic guarantees (shard routing is not security-bearing). Empty key returns 0; shards <= 1 always returns 0.

var shard = b.agent.orchestrator.shardFor("tenant-acme", 8);
// → integer in [0, 8)

b.agent.orchestrator.reseal(opts) #

stable0.14.12gdprsoc2
{
  store:       Object,   // { list(): rows[], set(name, row) } (the create() backend contract)
  oldRootJson: string,   // b.vault.getKeysJson() of the retired keypair
  newRootJson: string,   // b.vault.getKeysJson() of the new keypair
}

Re-seals every AAD-bound registry cell (tenantId / metadata) on an operator-supplied backend from the OLD vault keypair to the NEW one, out-of-band. The in-tree vault-key rotation pipeline only walks tables inside db.enc, so an operator-supplied orchestrator backend is unreachable to it — after a keypair rotation its cells would otherwise be orphaned under the retired root (CWE-320). Rebuilds each cell's AAD from the registered schema (one source of truth); only AAD-sealed cells are touched. The name row-identity column is the AAD anchor and is never sealed, so it is always present for the write-back.

await b.agent.orchestrator.reseal({ store: backend, oldRootJson: oldKeys, newRootJson: newKeys });
// → { table: "agent_orchestrator_registry", resealed: 4 }

Last updated 2026-08-08T16:39:49.652Z by seeder.