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:
parent
5dbd318a76
commit
00dc1ece89
9 changed files with 357 additions and 79 deletions
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue