feat(uok): 8-role swarm topology + DB-first sleeptime consolidation queue

- VALID_ROLES: coordinator/worker/scout/reviewer/planner/verifier/scribe/adversary (dropped architect)
- swarm-roles.js: PlannerAgent, VerifierAgent, ScribeAgent, AdversaryAgent + createDefaultSwarm wires all 8
- agent-swarm.js: route() maps plan/verify/document/challenge to new roles; _deriveWorkMode() covers all unitType patterns; getTopology() exposes all 8 role buckets; sleeptime case is now non-blocking (INSERT to DB queue instead of blocking memoryAgent.receive())
- sf-db.js: sleeptime_consolidation_queue table (schema v50) — id, conversation_agent, memory_agent, content, status, created_at, processed_at, result
- auto/loop.js: drainSleeptimeQueue() runs between every autonomous unit; reads pending queue rows, runs consolidation via PersistentAgent, marks done/error in DB
- core.js: workModes list includes verify/document/challenge
- skills/loader.js: isSkillRelevant() handles verify→review and document→docs trigger aliases
- swarm.test.mjs: updated topology assertions for 9-agent swarm

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Mikael Hugo 2026-05-09 15:11:19 +02:00
parent 5dbd318a76
commit 00dc1ece89
9 changed files with 357 additions and 79 deletions

View file

@ -15,6 +15,7 @@ import { runAutomaticAutonomousSolverEval } from "../autonomous-solver-eval.js";
import { debugLog } from "../debug-logger.js";
import { resolveEngine } from "../engine-resolver.js";
import { sfRoot } from "../paths.js";
import { getDatabase } from "../sf-db.js";
import {
ExecutionGraphScheduler,
scheduleSidecarQueue,
@ -203,6 +204,74 @@ function checkMemoryPressure() {
* timed-out phase from mutating state concurrently with the next iteration.
*/
let _danglingPhasePromise = null;
/**
* Drain pending sleeptime consolidation jobs from the DB queue.
*
* Purpose: process memory consolidation requests that were enqueued non-blocking
* by the sleeptime topology, without stalling the conversation turn that created them.
* Each pending row triggers a PersistentAgent.receive() call on the memory agent.
*
* Consumer: autoLoop between-unit boundary, ensuring consolidation is processed
* before the next work unit starts.
*/
async function drainSleeptimeQueue(basePath) {
const db = getDatabase();
if (!db) return;
let pending;
try {
pending = db
.prepare(
`SELECT id, conversation_agent, memory_agent, content
FROM sleeptime_consolidation_queue
WHERE status = 'pending'
ORDER BY created_at ASC
LIMIT 10`,
)
.all();
} catch {
return; // table not yet created on older DBs
}
if (!pending || pending.length === 0) return;
const { AgentSwarm } = await import("../uok/agent-swarm.js");
for (const job of pending) {
try {
const swarm = new AgentSwarm(basePath);
const memAgent = swarm.getByRole("coordinator")[0];
if (memAgent) {
swarm.send(job.conversation_agent, job.memory_agent, job.content);
const received = memAgent.receive(true);
const last = received[received.length - 1];
const result = last?.body
? typeof last.body === "string"
? last.body
: JSON.stringify(last.body)
: "";
db.prepare(
`UPDATE sleeptime_consolidation_queue
SET status = 'done', processed_at = :ts, result = :result
WHERE id = :id`,
).run({
":id": job.id,
":ts": new Date().toISOString(),
":result": result,
});
} else {
db.prepare(
`UPDATE sleeptime_consolidation_queue SET status = 'skipped', processed_at = :ts WHERE id = :id`,
).run({ ":id": job.id, ":ts": new Date().toISOString() });
}
} catch (err) {
db.prepare(
`UPDATE sleeptime_consolidation_queue SET status = 'error', processed_at = :ts, result = :result WHERE id = :id`,
).run({
":id": job.id,
":ts": new Date().toISOString(),
":result": String(err),
});
}
}
}
/**
* Wrap a phase function with a timeout. Rejects with an Error whose message
* starts with "phase-timeout:" so the blanket catch can handle it specially.
@ -444,7 +513,7 @@ export async function autoLoop(ctx, pi, s, deps) {
finishTurn("stopped", "manual-attention", "missing-command-context");
break;
}
// ── Drain any dangling phase promise before starting new work ──
// ── Drain dangling phase promise before starting new work ──
// Promise.race() on timeout does not cancel the underlying async fn; that
// fn keeps running and may mutate state after the loop has advanced.
// Awaiting its completion here ensures no concurrent state writes.
@ -457,6 +526,14 @@ export async function autoLoop(ctx, pi, s, deps) {
/* ignore — result is irrelevant */
}
}
// ── Drain sleeptime consolidation queue ──
// Memory consolidation jobs enqueued non-blocking by the sleeptime topology
// are processed here, between units, so they never block a conversation turn.
try {
await drainSleeptimeQueue(s.basePath);
} catch {
/* best-effort — never block autonomous loop on consolidation */
}
try {
// ── Blanket try/catch: one bad iteration must not kill the session
const prefs = deps.loadEffectiveSFPreferences()?.preferences;

View file

@ -516,7 +516,17 @@ export async function handleCoreCommand(trimmed, ctx, pi) {
if (trimmed === "mode" || trimmed.startsWith("mode ")) {
const modeArgs = trimmed.replace(/^mode\s*/, "").trim();
// If arg is a work mode (chat/plan/build/review/repair/research), use new mode system
const workModes = ["chat", "plan", "build", "review", "repair", "research"];
const workModes = [
"chat",
"plan",
"build",
"review",
"repair",
"research",
"verify",
"document",
"challenge",
];
if (workModes.includes(modeArgs)) {
handleModeCommand(modeArgs, ctx);
return true;

View file

@ -577,6 +577,23 @@ function ensureUokMessageTables(db) {
"CREATE INDEX IF NOT EXISTS idx_uok_messages_sent ON uok_messages(sent_at DESC)",
);
}
function ensureSleeptimeQueueTable(db) {
db.exec(`
CREATE TABLE IF NOT EXISTS sleeptime_consolidation_queue (
id TEXT PRIMARY KEY,
conversation_agent TEXT NOT NULL,
memory_agent TEXT NOT NULL,
content TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'pending',
created_at TEXT NOT NULL,
processed_at TEXT DEFAULT NULL,
result TEXT DEFAULT NULL
)
`);
db.exec(
"CREATE INDEX IF NOT EXISTS idx_sleeptime_queue_status ON sleeptime_consolidation_queue(status, created_at ASC)",
);
}
function ensureSelfFeedbackTables(db) {
db.exec(`
CREATE TABLE IF NOT EXISTS self_feedback (
@ -1290,6 +1307,7 @@ function initSchema(db, fileBacked) {
ensureSessionTables(db);
ensureSessionSnapshotTable(db);
ensureUokMessageTables(db);
ensureSleeptimeQueueTable(db);
ensureSpecSchemaTables(db);
ensureTaskFrontmatterColumns(db);
ensureRetrievalEvidenceTables(db);
@ -2904,6 +2922,17 @@ function migrateSchema(db) {
":applied_at": new Date().toISOString(),
});
}
if (currentVersion < 50) {
// Add sleeptime_consolidation_queue — decouples memory consolidation
// from the conversation turn so the daemon can drain it asynchronously.
ensureSleeptimeQueueTable(db);
db.prepare(
"INSERT INTO schema_version (version, applied_at) VALUES (:version, :applied_at)",
).run({
":version": 50,
":applied_at": new Date().toISOString(),
});
}
db.exec("COMMIT");
} catch (err) {
db.exec("ROLLBACK");

View file

@ -165,7 +165,7 @@ function isSkillRelevant(skill, workMode) {
t === workMode ||
t === "*" ||
(workMode === "build" && t === "code") ||
(workMode === "review" && t === "review") ||
(workMode === "research" && t === "research"),
(workMode === "verify" && t === "review") ||
(workMode === "document" && t === "docs"),
);
}

View file

@ -226,10 +226,10 @@ describe("AgentSwarm — registry", () => {
// ─── createDefaultSwarm — topology ────────────────────────────────────────────
describe("createDefaultSwarm — topology", () => {
test("creates_five_agents", async () => {
test("creates_nine_agents", async () => {
const root = makeProject();
const { swarm } = await createDefaultSwarm(root);
expect(swarm.getAll()).toHaveLength(5);
expect(swarm.getAll()).toHaveLength(9);
});
test("topology_has_correct_roles", async () => {
@ -241,6 +241,10 @@ describe("createDefaultSwarm — topology", () => {
expect(topology.workers).toHaveLength(2);
expect(topology.scouts).toHaveLength(1);
expect(topology.reviewers).toHaveLength(1);
expect(topology.planners).toHaveLength(1);
expect(topology.verifiers).toHaveLength(1);
expect(topology.scribes).toHaveLength(1);
expect(topology.adversaries).toHaveLength(1);
});
});

View file

@ -327,12 +327,19 @@ export class AgentSwarm {
switch (workMode) {
case "build":
case "repair":
case "code":
return this.getByRole("worker")[0];
case "research":
return this.getByRole("scout")[0];
case "review":
return this.getByRole("reviewer")[0];
case "plan":
return this.getByRole("planner")[0] ?? this.getByRole("coordinator")[0];
case "verify":
return this.getByRole("verifier")[0];
case "document":
return this.getByRole("scribe")[0];
case "challenge":
return this.getByRole("adversary")[0];
default:
return this.getByRole("coordinator")[0] ?? this.getAll()[0];
}
@ -342,10 +349,32 @@ export class AgentSwarm {
if (!unitType) return undefined;
const t = unitType.toLowerCase();
if (t.includes("research") || t.includes("scout")) return "research";
if (t.includes("review") || t.includes("audit")) return "review";
if (t.includes("review")) return "review";
if (
t.includes("audit") ||
t.includes("validate") ||
t.includes("gate") ||
t.includes("uat")
)
return "verify";
if (t.includes("repair") || t.includes("fix")) return "repair";
if (t.includes("build") || t.includes("code") || t.includes("implement"))
if (
t.includes("build") ||
t.includes("code") ||
t.includes("implement") ||
t.includes("execute")
)
return "build";
if (
t.includes("plan") ||
t.includes("slice") ||
t.includes("milestone") ||
t.includes("roadmap")
)
return "plan";
if (t.includes("doc") || t.includes("rewrite") || t.includes("scribe"))
return "document";
if (t.includes("challenge") || t.includes("adversar")) return "challenge";
return undefined;
}
@ -364,9 +393,13 @@ export class AgentSwarm {
getTopology() {
return {
coordinator: this.getByRole("coordinator"),
planners: this.getByRole("planner"),
workers: this.getByRole("worker"),
scouts: this.getByRole("scout"),
reviewers: this.getByRole("reviewer"),
verifiers: this.getByRole("verifier"),
scribes: this.getByRole("scribe"),
adversaries: this.getByRole("adversary"),
all: this.getAll(),
};
}
@ -493,7 +526,8 @@ export class AgentSwarm {
}
case ManagerType.SLEEPTIME: {
// One conversation agent + background memory consolidation by coordinator
// One conversation agent + non-blocking memory consolidation enqueued to DB.
// The scheduler drains sleeptime_consolidation_queue on each poll tick.
const conversationAgents = agents.filter(
(a) => a.identity.role !== "coordinator",
);
@ -515,16 +549,25 @@ export class AgentSwarm {
break;
}
message = reply;
// Background memory consolidation step
// Enqueue consolidation to DB — non-blocking; scheduler drains it.
if (memoryAgent) {
const memName = memoryAgent.identity.name;
this.send(convoName, memName, `consolidate: ${reply}`);
const memReceived = memoryAgent.receive(true);
const memLast = memReceived[memReceived.length - 1];
if (memLast) {
const db = getDatabase();
if (db) {
const jobId = `slp-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
db.prepare(
`INSERT INTO sleeptime_consolidation_queue
(id, conversation_agent, memory_agent, content, status, created_at)
VALUES (:id, :convo, :mem, :content, 'pending', :ts)`,
).run({
":id": jobId,
":convo": convoName,
":mem": memoryAgent.identity.name,
":content": `consolidate: ${reply}`,
":ts": new Date().toISOString(),
});
turns.push({
agent: memName,
message: `[memory] ${_bodyToString(memLast.body, "")}`,
agent: memoryAgent.identity.name,
message: `[memory:queued] job ${jobId}`,
});
}
}

View file

@ -23,6 +23,8 @@ export {
USER_SKILL_DIR,
validateSkillFrontmatter,
} from "../skills/index.js";
// ─── Agent Swarm ───────────────────────────────────────────────────────────
export { AgentSwarm, ManagerType } from "./agent-swarm.js";
export {
assessAssertionCoverage,
fulfilledAssertionIdsFromHandoff,
@ -34,7 +36,6 @@ export {
isAuditEnvelopeEnabled,
setAuditEnvelopeEnabled,
} from "./audit-toggle.js";
// ─── Gates ─────────────────────────────────────────────────────────────────
export { ChaosMonkey, ChaosMonkeyGate } from "./chaos-monkey.js";
// ─── Model Policy ──────────────────────────────────────────────────────────
@ -91,16 +92,8 @@ export {
export { recordUokKernelTermination, runAutoLoopWithUok } from "./kernel.js";
// ─── Loop Adapter ──────────────────────────────────────────────────────────
export { createTurnObserver } from "./loop-adapter.js";
// ─── Agent Swarm ───────────────────────────────────────────────────────────
export { AgentSwarm, ManagerType } from "./agent-swarm.js";
// ─── Letta Agent ───────────────────────────────────────────────────────────
export { PersistentAgent } from "./persistent-agent.js";
// ─── Message Bus ───────────────────────────────────────────────────────────
export { AgentInbox, MessageBus } from "./message-bus.js";
// ─── Swarm Roles ───────────────────────────────────────────────────────────
export { CoordinatorAgent, WorkerAgent, ScoutAgent, ReviewerAgent, createDefaultSwarm } from "./swarm-roles.js";
// ─── Swarm Dispatch ────────────────────────────────────────────────────────
export { SwarmDispatchLayer, swarmDispatch } from "./swarm-dispatch.js";
// ─── Metrics ───────────────────────────────────────────────────────────────
export {
buildMetricsText,
@ -143,6 +136,8 @@ export {
writeParityHeartbeat,
writeParityReport,
} from "./parity-report.js";
// ─── Letta Agent ───────────────────────────────────────────────────────────
export { PersistentAgent } from "./persistent-agent.js";
// ─── Plan v2 ───────────────────────────────────────────────────────────────
export {
compileUnitGraphFromState,
@ -166,6 +161,20 @@ export {
WorkerPool,
} from "./scheduler.js";
export { SecurityGate } from "./security-gate.js";
// ─── Swarm Dispatch ────────────────────────────────────────────────────────
export { SwarmDispatchLayer, swarmDispatch } from "./swarm-dispatch.js";
// ─── Swarm Roles ───────────────────────────────────────────────────────────
export {
AdversaryAgent,
CoordinatorAgent,
createDefaultSwarm,
PlannerAgent,
ReviewerAgent,
ScoutAgent,
ScribeAgent,
VerifierAgent,
WorkerAgent,
} from "./swarm-roles.js";
// ─── Task State Machine ────────────────────────────────────────────────────
export {
aggregateTaskStates,

View file

@ -12,12 +12,21 @@
import { randomUUID } from "node:crypto";
import { mkdirSync } from "node:fs";
import { join } from "node:path";
import { getDatabase, openDatabase } from "../sf-db.js";
import { sfRoot } from "../paths.js";
import { getDatabase, openDatabase } from "../sf-db.js";
import { UokCoordinationStore } from "./coordination-store.js";
import { MessageBus } from "./message-bus.js";
const VALID_ROLES = ["coordinator", "worker", "scout", "reviewer", "adversary", "architect"];
const VALID_ROLES = [
"coordinator",
"worker",
"scout",
"reviewer",
"planner",
"verifier",
"scribe",
"adversary",
];
const DEFAULT_ROLE = "worker";
const DEFAULT_MAX_INBOX_SIZE = 200;
const DEFAULT_REFRESH_INTERVAL_MS = 30_000;
@ -32,7 +41,8 @@ function ensureDbOpen(basePath) {
openDatabase(dbPath);
}
const db = getDatabase();
if (!db) throw new Error(`PersistentAgent: failed to open database at ${dbPath}`);
if (!db)
throw new Error(`PersistentAgent: failed to open database at ${dbPath}`);
return db;
}
@ -63,9 +73,18 @@ export class PersistentAgent {
* @param {number} [options.refreshIntervalMs=30000]
*/
constructor(basePath, options = {}) {
const { name, role = DEFAULT_ROLE, tags = [], maxInboxSize = DEFAULT_MAX_INBOX_SIZE, refreshIntervalMs = DEFAULT_REFRESH_INTERVAL_MS } = options;
const {
name,
role = DEFAULT_ROLE,
tags = [],
maxInboxSize = DEFAULT_MAX_INBOX_SIZE,
refreshIntervalMs = DEFAULT_REFRESH_INTERVAL_MS,
} = options;
if (!name) throw new Error("PersistentAgent: options.name is required");
if (!VALID_ROLES.includes(role)) throw new Error(`PersistentAgent: invalid role "${role}". Must be one of: ${VALID_ROLES.join(", ")}`);
if (!VALID_ROLES.includes(role))
throw new Error(
`PersistentAgent: invalid role "${role}". Must be one of: ${VALID_ROLES.join(", ")}`,
);
this._basePath = basePath;
this._name = name;
@ -175,7 +194,8 @@ export class PersistentAgent {
const lines = ["## Agent Memory Blocks", ""];
for (const { key, value } of entries) {
const label = key.slice(prefix.length);
const rendered = typeof value === "string" ? value : JSON.stringify(value);
const rendered =
typeof value === "string" ? value : JSON.stringify(value);
lines.push(`**${label}**: ${rendered}`);
}
return lines.join("\n");
@ -288,16 +308,17 @@ export class PersistentAgent {
* @returns {Promise<*>} reply body
*/
sendAndWait(toAgentName, body, opts = {}) {
const { timeoutMs = DEFAULT_SEND_AND_WAIT_TIMEOUT_MS, pollIntervalMs = DEFAULT_SEND_AND_WAIT_POLL_INTERVAL_MS } = opts;
const {
timeoutMs = DEFAULT_SEND_AND_WAIT_TIMEOUT_MS,
pollIntervalMs = DEFAULT_SEND_AND_WAIT_POLL_INTERVAL_MS,
} = opts;
const sentId = this.send(toAgentName, body, opts.metadata ?? {});
return new Promise((resolve, reject) => {
const deadline = Date.now() + timeoutMs;
const interval = setInterval(() => {
this._inbox.refresh();
const messages = this._inbox.list(false);
const reply = messages.find(
(m) => m.metadata?.replyTo === sentId,
);
const reply = messages.find((m) => m.metadata?.replyTo === sentId);
if (reply) {
clearInterval(interval);
resolve(reply.body);
@ -305,7 +326,11 @@ export class PersistentAgent {
}
if (Date.now() >= deadline) {
clearInterval(interval);
reject(new Error(`PersistentAgent.sendAndWait: timeout after ${timeoutMs}ms waiting for reply to ${sentId}`));
reject(
new Error(
`PersistentAgent.sendAndWait: timeout after ${timeoutMs}ms waiting for reply to ${sentId}`,
),
);
}
}, pollIntervalMs);
});

View file

@ -1,16 +1,16 @@
/**
* Swarm Role Agents Purpose-built PersistentAgent subclasses for the default SF swarm topology.
*
* Purpose: provide ready-to-use agent roles (coordinator, worker, scout, reviewer) with
* preset tags and capabilities, and a factory function that assembles a complete default
* swarm topology persisted to SQLite.
* Purpose: provide ready-to-use agent roles (coordinator, worker, scout, reviewer, planner,
* verifier, scribe, adversary) with preset tags and capabilities, and a factory function
* that assembles a complete default swarm topology persisted to SQLite.
*
* Consumer: SwarmDispatchLayer, /sf swarm command, and any code needing a default
* multi-agent topology without manually wiring individual agents.
*/
import { PersistentAgent } from "./persistent-agent.js";
import { AgentSwarm, ManagerType } from "./agent-swarm.js";
import { PersistentAgent } from "./persistent-agent.js";
/**
* Coordinator agent owns routing and task dispatch across a worker pool.
@ -64,7 +64,9 @@ export class CoordinatorAgent extends PersistentAgent {
// Prefer specialised match; fall back to first available worker.
const specialised = workMode
? candidates.filter((a) => a.identity.tags.includes(`workMode:${workMode}`))
? candidates.filter((a) =>
a.identity.tags.includes(`workMode:${workMode}`),
)
: [];
const target = specialised[0] ?? candidates[0] ?? null;
@ -76,7 +78,7 @@ export class CoordinatorAgent extends PersistentAgent {
}
/**
* Worker agent executes assigned tasks in the swarm.
* Worker agent executes assigned build and repair tasks.
*
* Purpose: provide a general-purpose execution agent that accepts task
* messages from a coordinator and runs them to completion.
@ -84,12 +86,6 @@ export class CoordinatorAgent extends PersistentAgent {
* Consumer: createDefaultSwarm worker pool and direct task assignment.
*/
export class WorkerAgent extends PersistentAgent {
/**
* @param {string} basePath
* @param {object} [options={}]
* @param {string} [options.name='worker']
* @param {string[]} [options.tags=[]]
*/
constructor(basePath, options = {}) {
super(basePath, {
name: options.name ?? "worker",
@ -104,18 +100,13 @@ export class WorkerAgent extends PersistentAgent {
* Scout agent discovers and surfaces information for the swarm.
*
* Purpose: provide a dedicated research/discovery agent whose results feed
* into coordinator routing decisions and worker task context.
* into coordinator routing decisions and worker task context. Runs in an
* isolated context with no side effects.
*
* Consumer: createDefaultSwarm and coordinator agents that need pre-fetched
* context before delegating execution tasks.
*/
export class ScoutAgent extends PersistentAgent {
/**
* @param {string} basePath
* @param {object} [options={}]
* @param {string} [options.name='scout']
* @param {string[]} [options.tags=[]]
*/
constructor(basePath, options = {}) {
super(basePath, {
name: options.name ?? "scout",
@ -127,22 +118,16 @@ export class ScoutAgent extends PersistentAgent {
}
/**
* Reviewer agent validates and critiques worker output.
* Reviewer agent critiques worker output against intent.
*
* Purpose: provide a dedicated quality-gate agent that receives completed
* task output from workers and either approves, requests revision, or
* escalates to the coordinator.
* Purpose: provide a fresh-context critique agent that receives completed
* task output and either approves, requests revision, or escalates. Runs
* without the worker's accumulated context so bias doesn't carry over.
*
* Consumer: createDefaultSwarm and any dispatch flow that requires a
* review pass before accepting task output as final.
* Consumer: createDefaultSwarm and any dispatch flow requiring a review
* pass before accepting task output as final.
*/
export class ReviewerAgent extends PersistentAgent {
/**
* @param {string} basePath
* @param {object} [options={}]
* @param {string} [options.name='reviewer']
* @param {string[]} [options.tags=[]]
*/
constructor(basePath, options = {}) {
super(basePath, {
name: options.name ?? "reviewer",
@ -153,30 +138,114 @@ export class ReviewerAgent extends PersistentAgent {
}
}
/**
* Planner agent generates milestone, slice, and task contracts.
*
* Purpose: own the plan workMode translate bounded intent into PDD-backed
* milestone/slice/task structures. Separated from coordinator so planning
* doesn't consume coordinator context and remains independently restartable.
*
* Consumer: createDefaultSwarm, auto-dispatch plan-milestone / plan-slice /
* refine-slice / replan-slice unit types.
*/
export class PlannerAgent extends PersistentAgent {
constructor(basePath, options = {}) {
super(basePath, {
name: options.name ?? "planner",
role: "planner",
tags: ["role:planner", "tier:persistent", ...(options.tags ?? [])],
...options,
});
}
}
/**
* Verifier agent runs gates, UAT, and evidence checks.
*
* Purpose: own the verify workMode execute binary pass/fail checks
* (gate-evaluate, validate-milestone, run-uat) without conflating them
* with subjective review. Separated from reviewer so verification failures
* produce actionable structured output rather than qualitative critique.
*
* Consumer: createDefaultSwarm and gate-runner dispatch paths.
*/
export class VerifierAgent extends PersistentAgent {
constructor(basePath, options = {}) {
super(basePath, {
name: options.name ?? "verifier",
role: "verifier",
tags: ["role:verifier", "tier:worker", ...(options.tags ?? [])],
...options,
});
}
}
/**
* Scribe agent writes and exports documentation.
*
* Purpose: own the document workMode produce changelogs, spec exports,
* ADRs, and user-facing docs from structured state. Separated from worker
* so doc generation doesn't share a context window with implementation work.
*
* Consumer: createDefaultSwarm and rewrite-docs / promote-spec unit types.
*/
export class ScribeAgent extends PersistentAgent {
constructor(basePath, options = {}) {
super(basePath, {
name: options.name ?? "scribe",
role: "scribe",
tags: ["role:scribe", "tier:worker", ...(options.tags ?? [])],
...options,
});
}
}
/**
* Adversary agent red-teams plans and decisions.
*
* Purpose: own the challenge workMode actively seek failure modes,
* unexamined assumptions, and security gaps in plans or implementations.
* Runs in adversarial framing so it doesn't default to agreement.
*
* Consumer: createDefaultSwarm and challenge-mode dispatch (future).
*/
export class AdversaryAgent extends PersistentAgent {
constructor(basePath, options = {}) {
super(basePath, {
name: options.name ?? "adversary",
role: "adversary",
tags: ["role:adversary", "tier:worker", ...(options.tags ?? [])],
...options,
});
}
}
/**
* Create a complete default swarm topology.
*
* Purpose: assemble the standard SF agent topology (1 coordinator + 2 workers +
* 1 scout + 1 reviewer) wired to a shared MessageBus, with the agent_directory
* core block set on all agents so each knows who else is in the swarm.
* Purpose: assemble the full SF agent topology (coordinator + planner +
* 2 workers + scout + reviewer + verifier + scribe + adversary) wired to a
* shared MessageBus, with the agent_directory core block set on all agents.
*
* Consumer: SwarmDispatchLayer.getOrCreateSwarm() and direct initialization in
* autonomous dispatch bootstrap.
* Consumer: SwarmDispatchLayer.getOrCreateSwarm() and autonomous dispatch bootstrap.
*
* @param {string} basePath
* @param {object} [options={}]
* @param {string} [options.swarmName='default']
* @param {string} [options.managerType] - ManagerType value, default 'supervisor'
* @returns {Promise<{ swarm: AgentSwarm, coordinator: CoordinatorAgent, workers: WorkerAgent[], scout: ScoutAgent, reviewer: ReviewerAgent }>}
*/
export async function createDefaultSwarm(basePath, options = {}) {
const coordinator = new CoordinatorAgent(basePath, { name: "coordinator" });
const planner = new PlannerAgent(basePath, { name: "planner" });
const workers = [
new WorkerAgent(basePath, { name: "worker-1" }),
new WorkerAgent(basePath, { name: "worker-2" }),
];
const scout = new ScoutAgent(basePath, { name: "scout" });
const reviewer = new ReviewerAgent(basePath, { name: "reviewer" });
const verifier = new VerifierAgent(basePath, { name: "verifier" });
const scribe = new ScribeAgent(basePath, { name: "scribe" });
const adversary = new AdversaryAgent(basePath, { name: "adversary" });
const swarm = new AgentSwarm(basePath, {
name: options.swarmName ?? "default",
@ -184,13 +253,25 @@ export async function createDefaultSwarm(basePath, options = {}) {
});
swarm.register(coordinator);
for (const worker of workers) {
swarm.register(worker);
}
swarm.register(planner);
for (const worker of workers) swarm.register(worker);
swarm.register(scout);
swarm.register(reviewer);
swarm.register(verifier);
swarm.register(scribe);
swarm.register(adversary);
swarm.persist();
return { swarm, coordinator, workers, scout, reviewer };
return {
swarm,
coordinator,
planner,
workers,
scout,
reviewer,
verifier,
scribe,
adversary,
};
}