feat(swarm): UOK-based swarm with PersistentAgent, AgentSwarm, and SwarmDispatchLayer
- PersistentAgent: stable identity across restarts, 3-tier memory (core blocks / recall / archival), durable SQLite inbox, sendAndWait request- reply, broadcast — all backed by UokCoordinationStore + MessageBus - AgentSwarm: Letta-style group topology with ManagerType enum (round_robin, supervisor, dynamic, sleeptime), tag-based routing, shared agent_directory block, persist/load round-trip - Role agents: CoordinatorAgent, WorkerAgent, ScoutAgent, ReviewerAgent extending PersistentAgent with preset tags + createDefaultSwarm factory (1 coordinator, 2 workers, 1 scout, 1 reviewer) - SwarmDispatchLayer: routes UOK DispatchEnvelopes by workMode/unitType to the correct role agent, module-level cache, swarmDispatch() convenience fn - 15 tests passing (identity persistence, messaging, registry, topology, dispatch routing) using real SQLite in tmp dirs - Fix: tsconfig.resources.json — add types:[node] for TypeScript 6 compat Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
parent
efa3ce4492
commit
848ac0dd99
7 changed files with 1722 additions and 0 deletions
332
src/resources/extensions/sf/tests/swarm.test.mjs
Normal file
332
src/resources/extensions/sf/tests/swarm.test.mjs
Normal file
|
|
@ -0,0 +1,332 @@
|
|||
/**
|
||||
* swarm.test.mjs — Full test suite for the SF swarm system.
|
||||
*
|
||||
* Purpose: verify PersistentAgent identity/messaging, AgentSwarm registry/routing,
|
||||
* createDefaultSwarm topology, and SwarmDispatchLayer envelope routing using real
|
||||
* SQLite in isolated tmp directories (no mocking).
|
||||
*
|
||||
* Consumer: CI unit-test suite (`npm run test:unit`).
|
||||
*/
|
||||
|
||||
import { mkdtempSync, rmSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { afterEach, describe, expect, test } from "vitest";
|
||||
import { closeDatabase } from "../sf-db.js";
|
||||
import { _clearSfRootCache } from "../paths.js";
|
||||
import { PersistentAgent } from "../uok/persistent-agent.js";
|
||||
import { AgentSwarm } from "../uok/agent-swarm.js";
|
||||
import {
|
||||
CoordinatorAgent,
|
||||
WorkerAgent,
|
||||
ScoutAgent,
|
||||
ReviewerAgent,
|
||||
createDefaultSwarm,
|
||||
} from "../uok/swarm-roles.js";
|
||||
import { SwarmDispatchLayer } from "../uok/swarm-dispatch.js";
|
||||
|
||||
// ─── Shared Setup ─────────────────────────────────────────────────────────────
|
||||
|
||||
const tmpRoots = [];
|
||||
|
||||
afterEach(() => {
|
||||
closeDatabase();
|
||||
_clearSfRootCache();
|
||||
for (const root of tmpRoots.splice(0)) {
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
function makeProject() {
|
||||
const root = mkdtempSync(join(tmpdir(), "sf-swarm-"));
|
||||
tmpRoots.push(root);
|
||||
return root;
|
||||
}
|
||||
|
||||
// ─── PersistentAgent — identity ───────────────────────────────────────────────
|
||||
|
||||
describe("PersistentAgent — identity", () => {
|
||||
test("init_when_new_creates_stable_agentId", () => {
|
||||
const root = makeProject();
|
||||
const agent = new PersistentAgent(root, { name: "alice", role: "worker" });
|
||||
expect(agent.identity.agentId).toMatch(/^agent-/);
|
||||
expect(agent.identity.name).toBe("alice");
|
||||
expect(agent.identity.role).toBe("worker");
|
||||
});
|
||||
|
||||
test("init_when_resumed_restores_same_agentId", () => {
|
||||
const root = makeProject();
|
||||
const agent1 = new PersistentAgent(root, { name: "bob", role: "worker" });
|
||||
const originalId = agent1.identity.agentId;
|
||||
|
||||
// Simulate process restart
|
||||
closeDatabase();
|
||||
_clearSfRootCache();
|
||||
|
||||
const agent2 = new PersistentAgent(root, { name: "bob", role: "worker" });
|
||||
expect(agent2.identity.agentId).toBe(originalId);
|
||||
});
|
||||
|
||||
test("coreBlock_when_set_persists_across_restart", () => {
|
||||
const root = makeProject();
|
||||
const agent1 = new PersistentAgent(root, { name: "carol", role: "worker" });
|
||||
agent1.setCoreBlock("notes", { text: "hello world" });
|
||||
|
||||
// Simulate process restart
|
||||
closeDatabase();
|
||||
_clearSfRootCache();
|
||||
|
||||
const agent2 = new PersistentAgent(root, { name: "carol", role: "worker" });
|
||||
expect(agent2.getCoreBlock("notes")).toEqual({ text: "hello world" });
|
||||
});
|
||||
});
|
||||
|
||||
// ─── PersistentAgent — messaging ──────────────────────────────────────────────
|
||||
|
||||
describe("PersistentAgent — messaging", () => {
|
||||
test("send_receive_delivers_message", () => {
|
||||
const root = makeProject();
|
||||
const agentA = new PersistentAgent(root, { name: "sender", role: "worker" });
|
||||
const agentB = new PersistentAgent(root, {
|
||||
name: "receiver",
|
||||
role: "worker",
|
||||
});
|
||||
|
||||
agentA.send("receiver", "hello-world-payload");
|
||||
|
||||
// Force inbox refresh (bypassing the 30s interval guard)
|
||||
agentB.inbox.refresh();
|
||||
const msgs = agentB.receive(true);
|
||||
|
||||
expect(msgs.length).toBeGreaterThanOrEqual(1);
|
||||
const found = msgs.find((m) => m.body === "hello-world-payload");
|
||||
expect(found).toBeDefined();
|
||||
});
|
||||
|
||||
test("markRead_when_called_hides_from_unread", () => {
|
||||
const root = makeProject();
|
||||
const agentA = new PersistentAgent(root, {
|
||||
name: "sender2",
|
||||
role: "worker",
|
||||
});
|
||||
const agentB = new PersistentAgent(root, {
|
||||
name: "receiver2",
|
||||
role: "worker",
|
||||
});
|
||||
|
||||
agentA.send("receiver2", "ping");
|
||||
|
||||
agentB.inbox.refresh();
|
||||
const unread = agentB.receive(true);
|
||||
expect(unread.length).toBeGreaterThanOrEqual(1);
|
||||
|
||||
agentB.markRead(unread[0].id);
|
||||
|
||||
agentB.inbox.refresh();
|
||||
expect(agentB.receive(true)).toHaveLength(0);
|
||||
});
|
||||
|
||||
test("broadcast_delivers_to_all_recipients", () => {
|
||||
const root = makeProject();
|
||||
const agentA = new PersistentAgent(root, {
|
||||
name: "broadcaster",
|
||||
role: "coordinator",
|
||||
});
|
||||
const agentB = new PersistentAgent(root, { name: "recv-b", role: "worker" });
|
||||
const agentC = new PersistentAgent(root, { name: "recv-c", role: "worker" });
|
||||
|
||||
agentA.broadcast("announcement", {}, ["recv-b", "recv-c"]);
|
||||
|
||||
agentB.inbox.refresh();
|
||||
agentC.inbox.refresh();
|
||||
|
||||
expect(agentB.receive(true)).toHaveLength(1);
|
||||
expect(agentC.receive(true)).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── AgentSwarm — registry ────────────────────────────────────────────────────
|
||||
|
||||
describe("AgentSwarm — registry", () => {
|
||||
test("register_adds_agent_to_roster", () => {
|
||||
const root = makeProject();
|
||||
const swarm = new AgentSwarm(root, { name: "test-swarm" });
|
||||
const worker = new PersistentAgent(root, {
|
||||
name: "worker",
|
||||
role: "worker",
|
||||
});
|
||||
|
||||
swarm.register(worker);
|
||||
|
||||
expect(swarm.getAll()).toHaveLength(1);
|
||||
expect(swarm.get("worker").identity.name).toBe("worker");
|
||||
});
|
||||
|
||||
test("getByRole_filters_correctly", () => {
|
||||
const root = makeProject();
|
||||
const swarm = new AgentSwarm(root, { name: "test-swarm-roles" });
|
||||
const coord = new PersistentAgent(root, {
|
||||
name: "coord",
|
||||
role: "coordinator",
|
||||
});
|
||||
const w1 = new PersistentAgent(root, { name: "w1", role: "worker" });
|
||||
const w2 = new PersistentAgent(root, { name: "w2", role: "worker" });
|
||||
|
||||
swarm.register(coord);
|
||||
swarm.register(w1);
|
||||
swarm.register(w2);
|
||||
|
||||
expect(swarm.getByRole("worker")).toHaveLength(2);
|
||||
expect(swarm.getByRole("coordinator")).toHaveLength(1);
|
||||
});
|
||||
|
||||
test("persist_and_load_round_trips", () => {
|
||||
const root = makeProject();
|
||||
const swarm = new AgentSwarm(root, { name: "persist-swarm" });
|
||||
const wA = new PersistentAgent(root, { name: "wA", role: "worker" });
|
||||
const wB = new PersistentAgent(root, { name: "wB", role: "worker" });
|
||||
|
||||
swarm.register(wA);
|
||||
swarm.register(wB);
|
||||
swarm.persist();
|
||||
|
||||
// Simulate restart
|
||||
closeDatabase();
|
||||
_clearSfRootCache();
|
||||
|
||||
const loaded = AgentSwarm.load(root, "persist-swarm");
|
||||
const names = loaded
|
||||
.getAll()
|
||||
.map((a) => a.identity.name)
|
||||
.sort();
|
||||
expect(names).toEqual(["wA", "wB"]);
|
||||
});
|
||||
|
||||
test("agentDirectory_injected_on_register", () => {
|
||||
const root = makeProject();
|
||||
const swarm = new AgentSwarm(root, { name: "dir-swarm" });
|
||||
const a1 = new PersistentAgent(root, { name: "dir-a1", role: "worker" });
|
||||
const a2 = new PersistentAgent(root, { name: "dir-a2", role: "worker" });
|
||||
|
||||
swarm.register(a1);
|
||||
swarm.register(a2);
|
||||
|
||||
const dir1 = JSON.parse(a1.getCoreBlock("agent_directory"));
|
||||
const dir2 = JSON.parse(a2.getCoreBlock("agent_directory"));
|
||||
const names1 = dir1.map((e) => e.name);
|
||||
const names2 = dir2.map((e) => e.name);
|
||||
|
||||
expect(names1).toContain("dir-a1");
|
||||
expect(names1).toContain("dir-a2");
|
||||
expect(names2).toContain("dir-a1");
|
||||
expect(names2).toContain("dir-a2");
|
||||
});
|
||||
});
|
||||
|
||||
// ─── createDefaultSwarm — topology ────────────────────────────────────────────
|
||||
|
||||
describe("createDefaultSwarm — topology", () => {
|
||||
test("creates_five_agents", async () => {
|
||||
const root = makeProject();
|
||||
const { swarm } = await createDefaultSwarm(root);
|
||||
expect(swarm.getAll()).toHaveLength(5);
|
||||
});
|
||||
|
||||
test("topology_has_correct_roles", async () => {
|
||||
const root = makeProject();
|
||||
const { swarm } = await createDefaultSwarm(root);
|
||||
const topology = swarm.getTopology();
|
||||
|
||||
expect(topology.coordinator).toHaveLength(1);
|
||||
expect(topology.workers).toHaveLength(2);
|
||||
expect(topology.scouts).toHaveLength(1);
|
||||
expect(topology.reviewers).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── SwarmDispatchLayer — routing ─────────────────────────────────────────────
|
||||
|
||||
describe("SwarmDispatchLayer — routing", () => {
|
||||
test("dispatch_build_envelope_routes_to_worker", async () => {
|
||||
const root = makeProject();
|
||||
const layer = new SwarmDispatchLayer(root);
|
||||
|
||||
const result = await layer.dispatch({
|
||||
unitId: "task-build-1",
|
||||
unitType: "task",
|
||||
workMode: "build",
|
||||
payload: "build task data",
|
||||
priority: 5,
|
||||
scope: "test-scope",
|
||||
});
|
||||
|
||||
expect(result.messageId).toBeTruthy();
|
||||
expect(result.targetAgent).toBeTruthy();
|
||||
|
||||
const swarm = await layer.getOrCreateSwarm();
|
||||
const agent = swarm.get(result.targetAgent);
|
||||
expect(agent).toBeDefined();
|
||||
expect(agent.identity.role).toBe("worker");
|
||||
});
|
||||
|
||||
test("dispatch_review_envelope_routes_to_reviewer", async () => {
|
||||
const root = makeProject();
|
||||
const layer = new SwarmDispatchLayer(root);
|
||||
|
||||
const result = await layer.dispatch({
|
||||
unitId: "task-review-1",
|
||||
unitType: "task",
|
||||
workMode: "review",
|
||||
payload: "review task data",
|
||||
priority: 5,
|
||||
scope: "test-scope",
|
||||
});
|
||||
|
||||
expect(result.messageId).toBeTruthy();
|
||||
|
||||
const swarm = await layer.getOrCreateSwarm();
|
||||
const agent = swarm.get(result.targetAgent);
|
||||
expect(agent).toBeDefined();
|
||||
expect(agent.identity.role).toBe("reviewer");
|
||||
});
|
||||
|
||||
test("dispatchBatch_dispatches_all", async () => {
|
||||
const root = makeProject();
|
||||
const layer = new SwarmDispatchLayer(root);
|
||||
|
||||
const envelopes = [
|
||||
{
|
||||
unitId: "t1",
|
||||
unitType: "task",
|
||||
workMode: "build",
|
||||
payload: "task-1",
|
||||
priority: 1,
|
||||
scope: "test",
|
||||
},
|
||||
{
|
||||
unitId: "t2",
|
||||
unitType: "task",
|
||||
workMode: "build",
|
||||
payload: "task-2",
|
||||
priority: 1,
|
||||
scope: "test",
|
||||
},
|
||||
{
|
||||
unitId: "t3",
|
||||
unitType: "task",
|
||||
workMode: "build",
|
||||
payload: "task-3",
|
||||
priority: 1,
|
||||
scope: "test",
|
||||
},
|
||||
];
|
||||
|
||||
const results = await layer.dispatchBatch(envelopes);
|
||||
|
||||
expect(results).toHaveLength(3);
|
||||
for (const r of results) {
|
||||
expect(r.messageId).toBeTruthy();
|
||||
expect(r.targetAgent).toBeTruthy();
|
||||
}
|
||||
});
|
||||
});
|
||||
620
src/resources/extensions/sf/uok/agent-swarm.js
Normal file
620
src/resources/extensions/sf/uok/agent-swarm.js
Normal file
|
|
@ -0,0 +1,620 @@
|
|||
/**
|
||||
* agent-swarm.js — Multi-agent group orchestrator with Letta-style group topology.
|
||||
*
|
||||
* Purpose: manage a named group of PersistentAgents with durable SQLite-backed registry,
|
||||
* tag-based routing, and group turn execution. The swarm persists agent registrations
|
||||
* across restarts and coordinates via the shared MessageBus.
|
||||
*
|
||||
* Consumer: SwarmDispatchLayer, createDefaultSwarm factory, and direct multi-agent
|
||||
* orchestration flows.
|
||||
*/
|
||||
|
||||
import { mkdirSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { sfRoot } from "../paths.js";
|
||||
import { getDatabase, openDatabase } from "../sf-db.js";
|
||||
import { UokCoordinationStore } from "./coordination-store.js";
|
||||
import { MessageBus } from "./message-bus.js";
|
||||
import { PersistentAgent } from "./persistent-agent.js";
|
||||
|
||||
/**
|
||||
* Letta-style group topology manager types.
|
||||
*
|
||||
* Purpose: enumerate valid routing strategies so callers get a type-safe,
|
||||
* discoverable set of options rather than ad-hoc strings.
|
||||
*
|
||||
* Consumer: AgentSwarm constructor and run() dispatch logic.
|
||||
*/
|
||||
export const ManagerType = Object.freeze({
|
||||
ROUND_ROBIN: "round_robin",
|
||||
SUPERVISOR: "supervisor",
|
||||
DYNAMIC: "dynamic",
|
||||
SLEEPTIME: "sleeptime",
|
||||
});
|
||||
|
||||
const DEFAULT_MANAGER_TYPE = ManagerType.SUPERVISOR;
|
||||
const DEFAULT_TERMINATION_TOKEN = "DONE!";
|
||||
const DEFAULT_MAX_TURNS = 20;
|
||||
|
||||
function ensureDbOpen(basePath) {
|
||||
const dir = sfRoot(basePath);
|
||||
mkdirSync(dir, { recursive: true });
|
||||
const dbPath = join(dir, "sf.db");
|
||||
if (!getDatabase()) {
|
||||
openDatabase(dbPath);
|
||||
}
|
||||
const db = getDatabase();
|
||||
if (!db) throw new Error(`AgentSwarm: failed to open database at ${dbPath}`);
|
||||
return db;
|
||||
}
|
||||
|
||||
/**
|
||||
* AgentSwarm — Multi-agent group with MessageBus coordination and Letta-style topology.
|
||||
*
|
||||
* Purpose: manage a named group of PersistentAgents with durable SQLite-backed registry,
|
||||
* tag-based routing, and group turn execution. The swarm persists agent registrations
|
||||
* across restarts and coordinates via the shared MessageBus.
|
||||
*
|
||||
* Consumer: SwarmDispatchLayer, createDefaultSwarm factory, and direct multi-agent
|
||||
* orchestration flows.
|
||||
*/
|
||||
export class AgentSwarm {
|
||||
/**
|
||||
* Create or resume a named agent swarm.
|
||||
*
|
||||
* Purpose: initialize swarm identity and reload any previously registered
|
||||
* agents from the durable KV registry so swarm state survives restarts.
|
||||
*
|
||||
* Consumer: orchestration entry points and swarm factory functions.
|
||||
*
|
||||
* @param {string} basePath - project root used to locate `.sf/sf.db`
|
||||
* @param {object} options
|
||||
* @param {string} options.name - stable swarm identity key (required)
|
||||
* @param {string} [options.managerType='supervisor'] - routing topology
|
||||
* @param {string} [options.terminationToken='DONE!'] - token that ends a run loop
|
||||
* @param {number} [options.maxTurns=20] - max turn iterations per run()
|
||||
*/
|
||||
constructor(basePath, options = {}) {
|
||||
const {
|
||||
name,
|
||||
managerType = DEFAULT_MANAGER_TYPE,
|
||||
terminationToken = DEFAULT_TERMINATION_TOKEN,
|
||||
maxTurns = DEFAULT_MAX_TURNS,
|
||||
} = options;
|
||||
if (!name) throw new Error("AgentSwarm: options.name is required");
|
||||
if (!Object.values(ManagerType).includes(managerType)) {
|
||||
throw new Error(
|
||||
`AgentSwarm: invalid managerType "${managerType}". Must be one of: ${Object.values(ManagerType).join(", ")}`,
|
||||
);
|
||||
}
|
||||
|
||||
this._basePath = basePath;
|
||||
this._name = name;
|
||||
this._managerType = managerType;
|
||||
this._terminationToken = terminationToken;
|
||||
this._maxTurns = maxTurns;
|
||||
|
||||
/** @type {Map<string, PersistentAgent>} */
|
||||
this._agents = new Map();
|
||||
|
||||
const db = ensureDbOpen(basePath);
|
||||
this._store = new UokCoordinationStore(db);
|
||||
this._bus = new MessageBus(basePath);
|
||||
|
||||
// Persist initial config
|
||||
this._store.set(this._configKey(), {
|
||||
name,
|
||||
managerType,
|
||||
terminationToken,
|
||||
maxTurns,
|
||||
});
|
||||
|
||||
// Reload any previously registered agents from the durable registry
|
||||
const registry = this._store.get(this._registryKey());
|
||||
if (registry && Array.isArray(registry.agents)) {
|
||||
for (const entry of registry.agents) {
|
||||
const agent = new PersistentAgent(basePath, {
|
||||
name: entry.name,
|
||||
role: entry.role,
|
||||
tags: entry.tags ?? [],
|
||||
});
|
||||
this._agents.set(entry.name, agent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─── KV Key Helpers ────────────────────────────────────────────────────────
|
||||
|
||||
_registryKey() {
|
||||
return `swarm:${this._name}:registry`;
|
||||
}
|
||||
|
||||
_configKey() {
|
||||
return `swarm:${this._name}:config`;
|
||||
}
|
||||
|
||||
_directoryKey() {
|
||||
return `swarm:${this._name}:directory`;
|
||||
}
|
||||
|
||||
// ─── Agent Registration ────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Add a PersistentAgent to the swarm and broadcast the updated agent directory.
|
||||
*
|
||||
* Purpose: maintain a live roster of agents, keeping every agent's
|
||||
* agent_directory core block in sync so each agent knows its peers.
|
||||
*
|
||||
* Consumer: swarm setup code and dynamic agent onboarding flows.
|
||||
*
|
||||
* @param {PersistentAgent} agent
|
||||
*/
|
||||
register(agent) {
|
||||
if (!agent || typeof agent.identity !== "object") {
|
||||
throw new Error(
|
||||
"AgentSwarm.register: argument must be a PersistentAgent instance",
|
||||
);
|
||||
}
|
||||
const { name } = agent.identity;
|
||||
this._agents.set(name, agent);
|
||||
|
||||
// Rebuild and persist the directory
|
||||
const directory = this._buildDirectory();
|
||||
this._store.set(this._directoryKey(), directory);
|
||||
|
||||
// Push updated directory to every registered agent's core block
|
||||
const directoryJson = JSON.stringify(directory);
|
||||
for (const a of this._agents.values()) {
|
||||
a.setCoreBlock("agent_directory", directoryJson);
|
||||
}
|
||||
|
||||
this.persist();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a registered agent by name.
|
||||
*
|
||||
* Purpose: give callers direct access to a named swarm member for targeted
|
||||
* operations without exposing the internal agent map.
|
||||
*
|
||||
* Consumer: routing logic, tests, and external coordination flows.
|
||||
*
|
||||
* @param {string} agentName
|
||||
* @returns {PersistentAgent | undefined}
|
||||
*/
|
||||
get(agentName) {
|
||||
return this._agents.get(agentName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all registered agents.
|
||||
*
|
||||
* Purpose: provide a snapshot of the full swarm roster for iteration or
|
||||
* broadcast operations.
|
||||
*
|
||||
* Consumer: run() loops, broadcast helpers, and topology queries.
|
||||
*
|
||||
* @returns {PersistentAgent[]}
|
||||
*/
|
||||
getAll() {
|
||||
return [...this._agents.values()];
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all agents whose identity.role matches the given role.
|
||||
*
|
||||
* Purpose: enable role-based routing so coordinators, workers, scouts,
|
||||
* and reviewers can be selected without hardcoding agent names.
|
||||
*
|
||||
* Consumer: route(), getTopology(), and dynamic dispatch flows.
|
||||
*
|
||||
* @param {string} role
|
||||
* @returns {PersistentAgent[]}
|
||||
*/
|
||||
getByRole(role) {
|
||||
return this.getAll().filter((a) => a.identity.role === role);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return agents matching Letta-style tag filters.
|
||||
*
|
||||
* Purpose: support fine-grained tag-based routing (matchAll = AND,
|
||||
* matchSome = OR) so callers can select agents by capability labels.
|
||||
*
|
||||
* Consumer: sendToAgentsMatchingTags and dynamic swarm routing.
|
||||
*
|
||||
* @param {string[]} [matchAll=[]] - agent must have ALL of these tags
|
||||
* @param {string[]} [matchSome=[]] - agent must have AT LEAST ONE of these tags
|
||||
* @returns {PersistentAgent[]}
|
||||
*/
|
||||
getByTags(matchAll = [], matchSome = []) {
|
||||
return this.getAll().filter((a) => {
|
||||
const tags = a.identity.tags ?? [];
|
||||
const allMatch =
|
||||
matchAll.length === 0 || matchAll.every((t) => tags.includes(t));
|
||||
const someMatch =
|
||||
matchSome.length === 0 || matchSome.some((t) => tags.includes(t));
|
||||
return allMatch && someMatch;
|
||||
});
|
||||
}
|
||||
|
||||
// ─── Messaging ─────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Send a message from one registered agent to another.
|
||||
*
|
||||
* Purpose: route a point-to-point swarm message via the shared MessageBus
|
||||
* so it is durably stored and survives process restarts.
|
||||
*
|
||||
* Consumer: swarm orchestration flows delegating work between agents.
|
||||
*
|
||||
* @param {string} fromName
|
||||
* @param {string} toName
|
||||
* @param {*} body
|
||||
* @param {object} [metadata={}]
|
||||
* @returns {string} message ID
|
||||
*/
|
||||
send(fromName, toName, body, metadata = {}) {
|
||||
return this._bus.send(
|
||||
`agent:${fromName}`,
|
||||
`agent:${toName}`,
|
||||
body,
|
||||
metadata,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcast a message from one agent to all registered agents.
|
||||
*
|
||||
* Purpose: fan-out a notification or task to the full swarm in one call.
|
||||
*
|
||||
* Consumer: coordinator agents distributing context or system-level events.
|
||||
*
|
||||
* @param {string} fromName
|
||||
* @param {*} body
|
||||
* @param {object} [metadata={}]
|
||||
* @returns {string[]} message IDs
|
||||
*/
|
||||
broadcast(fromName, body, metadata = {}) {
|
||||
const recipients = this.getAll()
|
||||
.map((a) => a.identity.name)
|
||||
.filter((n) => n !== fromName);
|
||||
return this._bus.broadcast(
|
||||
`agent:${fromName}`,
|
||||
recipients.map((n) => `agent:${n}`),
|
||||
body,
|
||||
metadata,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a message to all agents matching the given tag filters.
|
||||
*
|
||||
* Purpose: target capability-tagged subsets of the swarm without the caller
|
||||
* knowing individual agent names.
|
||||
*
|
||||
* Consumer: dynamic dispatch flows that route by capability rather than identity.
|
||||
*
|
||||
* @param {string} fromName
|
||||
* @param {*} body
|
||||
* @param {string[]} [matchAll=[]]
|
||||
* @param {string[]} [matchSome=[]]
|
||||
* @returns {string[]} message IDs
|
||||
*/
|
||||
sendToAgentsMatchingTags(fromName, body, matchAll = [], matchSome = []) {
|
||||
const targets = this.getByTags(matchAll, matchSome).filter(
|
||||
(a) => a.identity.name !== fromName,
|
||||
);
|
||||
return targets.map((a) => this.send(fromName, a.identity.name, body));
|
||||
}
|
||||
|
||||
// ─── Routing ───────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Route a DispatchEnvelope to the appropriate swarm agent by workMode/unitType.
|
||||
*
|
||||
* Purpose: map task routing decisions to the correct role agent so callers
|
||||
* don't need to know the swarm roster or role layout.
|
||||
*
|
||||
* Consumer: SwarmDispatchLayer and autonomous dispatch loops.
|
||||
*
|
||||
* @param {{ unitType?: string, workMode?: string }} envelope
|
||||
* @returns {PersistentAgent | undefined}
|
||||
*/
|
||||
route(envelope) {
|
||||
const workMode =
|
||||
envelope.workMode ?? this._deriveWorkMode(envelope.unitType);
|
||||
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];
|
||||
default:
|
||||
return this.getByRole("coordinator")[0] ?? this.getAll()[0];
|
||||
}
|
||||
}
|
||||
|
||||
_deriveWorkMode(unitType) {
|
||||
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("repair") || t.includes("fix")) return "repair";
|
||||
if (t.includes("build") || t.includes("code") || t.includes("implement"))
|
||||
return "build";
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// ─── Topology ──────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Return a structured view of the swarm by role.
|
||||
*
|
||||
* Purpose: give callers a single snapshot of the swarm topology for
|
||||
* display, routing, and diagnostic purposes without exposing internals.
|
||||
*
|
||||
* Consumer: run() orchestration and swarm health checks.
|
||||
*
|
||||
* @returns {{ coordinator: PersistentAgent[], workers: PersistentAgent[], scouts: PersistentAgent[], reviewers: PersistentAgent[], all: PersistentAgent[] }}
|
||||
*/
|
||||
getTopology() {
|
||||
return {
|
||||
coordinator: this.getByRole("coordinator"),
|
||||
workers: this.getByRole("worker"),
|
||||
scouts: this.getByRole("scout"),
|
||||
reviewers: this.getByRole("reviewer"),
|
||||
all: this.getAll(),
|
||||
};
|
||||
}
|
||||
|
||||
// ─── Group Turn Loop ───────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Execute a group turn loop over the swarm agents.
|
||||
*
|
||||
* Purpose: drive a multi-turn conversation across agents using the configured
|
||||
* topology (round_robin, supervisor, dynamic, sleeptime) and return the full
|
||||
* turn history with termination status.
|
||||
*
|
||||
* Consumer: autonomous dispatch, eval harness, and multi-agent task runners.
|
||||
*
|
||||
* @param {string} initialMessage
|
||||
* @param {object} [opts]
|
||||
* @param {string} [opts.from] - agent name to send as
|
||||
* @param {number} [opts.maxTurns] - override swarm default
|
||||
* @param {number} [opts.timeout] - timeout in ms (informational; not enforced at socket level)
|
||||
* @returns {{ turns: Array<{agent: string, message: string}>, terminated: boolean, reason: string }}
|
||||
*/
|
||||
run(initialMessage, opts = {}) {
|
||||
const maxTurns = opts.maxTurns ?? this._maxTurns;
|
||||
const fromName = opts.from ?? "external";
|
||||
const terminationToken = this._terminationToken;
|
||||
|
||||
const turns = [];
|
||||
let terminated = false;
|
||||
let reason = "max_turns_reached";
|
||||
|
||||
const agents = this.getAll();
|
||||
if (agents.length === 0) {
|
||||
return { turns, terminated: false, reason: "no_agents" };
|
||||
}
|
||||
|
||||
switch (this._managerType) {
|
||||
case ManagerType.ROUND_ROBIN: {
|
||||
let message = initialMessage;
|
||||
for (let i = 0; i < maxTurns; i++) {
|
||||
const agent = agents[i % agents.length];
|
||||
const agentName = agent.identity.name;
|
||||
this.send(fromName, agentName, message);
|
||||
const received = agent.receive(false);
|
||||
const last = received[received.length - 1];
|
||||
const reply = _bodyToString(last?.body, message);
|
||||
turns.push({ agent: agentName, message: reply });
|
||||
if (reply.includes(terminationToken)) {
|
||||
terminated = true;
|
||||
reason = "termination_token";
|
||||
break;
|
||||
}
|
||||
message = reply;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case ManagerType.SUPERVISOR: {
|
||||
const { coordinator } = this.getTopology();
|
||||
const supervisor = coordinator[0] ?? agents[0];
|
||||
const supervisorName = supervisor.identity.name;
|
||||
this.send(fromName, supervisorName, initialMessage);
|
||||
|
||||
let message = initialMessage;
|
||||
for (let i = 0; i < maxTurns; i++) {
|
||||
const received = supervisor.receive(true);
|
||||
const last = received[received.length - 1];
|
||||
const reply = _bodyToString(last?.body, message);
|
||||
turns.push({ agent: supervisorName, message: reply });
|
||||
if (reply.includes(terminationToken)) {
|
||||
terminated = true;
|
||||
reason = "termination_token";
|
||||
break;
|
||||
}
|
||||
|
||||
// Supervisor routes to a worker (cycle through workers)
|
||||
const workers = this.getByRole("worker");
|
||||
if (workers.length > 0) {
|
||||
const worker = workers[i % workers.length];
|
||||
const workerName = worker.identity.name;
|
||||
this.send(supervisorName, workerName, reply);
|
||||
const workerMessages = worker.receive(true);
|
||||
const workerLast = workerMessages[workerMessages.length - 1];
|
||||
const workerReply = _bodyToString(workerLast?.body, reply);
|
||||
turns.push({ agent: workerName, message: workerReply });
|
||||
if (workerReply.includes(terminationToken)) {
|
||||
terminated = true;
|
||||
reason = "termination_token";
|
||||
break;
|
||||
}
|
||||
// Route worker result back to supervisor for next iteration
|
||||
this.send(workerName, supervisorName, workerReply);
|
||||
message = workerReply;
|
||||
} else {
|
||||
message = reply;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case ManagerType.DYNAMIC: {
|
||||
// Each turn: give every agent a chance to reply; first with token ends loop
|
||||
let message = initialMessage;
|
||||
for (let i = 0; i < maxTurns; i++) {
|
||||
let tokenFound = false;
|
||||
for (const agent of agents) {
|
||||
const agentName = agent.identity.name;
|
||||
this.send(fromName, agentName, message);
|
||||
const received = agent.receive(false);
|
||||
const last = received[received.length - 1];
|
||||
const reply = _bodyToString(last?.body, message);
|
||||
turns.push({ agent: agentName, message: reply });
|
||||
if (reply.includes(terminationToken)) {
|
||||
terminated = true;
|
||||
reason = "termination_token";
|
||||
tokenFound = true;
|
||||
break;
|
||||
}
|
||||
message = reply;
|
||||
}
|
||||
if (tokenFound) break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case ManagerType.SLEEPTIME: {
|
||||
// One conversation agent + background memory consolidation by coordinator
|
||||
const conversationAgents = agents.filter(
|
||||
(a) => a.identity.role !== "coordinator",
|
||||
);
|
||||
const memoryAgent = this.getByRole("coordinator")[0];
|
||||
const convoAgent = conversationAgents[0] ?? agents[0];
|
||||
const convoName = convoAgent.identity.name;
|
||||
|
||||
let message = initialMessage;
|
||||
this.send(fromName, convoName, message);
|
||||
|
||||
for (let i = 0; i < maxTurns; i++) {
|
||||
const received = convoAgent.receive(true);
|
||||
const last = received[received.length - 1];
|
||||
const reply = _bodyToString(last?.body, message);
|
||||
turns.push({ agent: convoName, message: reply });
|
||||
if (reply.includes(terminationToken)) {
|
||||
terminated = true;
|
||||
reason = "termination_token";
|
||||
break;
|
||||
}
|
||||
message = reply;
|
||||
// Background memory consolidation step
|
||||
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) {
|
||||
turns.push({
|
||||
agent: memName,
|
||||
message: `[memory] ${_bodyToString(memLast.body, "")}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return { turns, terminated, reason };
|
||||
}
|
||||
|
||||
// ─── Persistence ───────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Save the current agent registry snapshot to the KV store.
|
||||
*
|
||||
* Purpose: make swarm membership durable so agents can be rehydrated on
|
||||
* restart without re-registering them programmatically.
|
||||
*
|
||||
* Consumer: register() and any code that mutates swarm membership.
|
||||
*/
|
||||
persist() {
|
||||
const agents = this.getAll().map((a) => ({
|
||||
name: a.identity.name,
|
||||
role: a.identity.role,
|
||||
tags: a.identity.tags ?? [],
|
||||
}));
|
||||
this._store.set(this._registryKey(), {
|
||||
name: this._name,
|
||||
managerType: this._managerType,
|
||||
agents,
|
||||
config: {
|
||||
terminationToken: this._terminationToken,
|
||||
maxTurns: this._maxTurns,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Rehydrate an AgentSwarm from a previously persisted registry.
|
||||
*
|
||||
* Purpose: restore swarm state across process restarts without requiring
|
||||
* callers to re-register agents explicitly.
|
||||
*
|
||||
* Consumer: orchestration bootstrap and restart recovery paths.
|
||||
*
|
||||
* @param {string} basePath
|
||||
* @param {string} name
|
||||
* @returns {AgentSwarm}
|
||||
*/
|
||||
static load(basePath, name) {
|
||||
const db = ensureDbOpen(basePath);
|
||||
const store = new UokCoordinationStore(db);
|
||||
const registryKey = `swarm:${name}:registry`;
|
||||
const saved = store.get(registryKey);
|
||||
|
||||
if (!saved) {
|
||||
return new AgentSwarm(basePath, { name });
|
||||
}
|
||||
|
||||
const swarm = new AgentSwarm(basePath, {
|
||||
name: saved.name ?? name,
|
||||
managerType: saved.managerType ?? DEFAULT_MANAGER_TYPE,
|
||||
terminationToken:
|
||||
saved.config?.terminationToken ?? DEFAULT_TERMINATION_TOKEN,
|
||||
maxTurns: saved.config?.maxTurns ?? DEFAULT_MAX_TURNS,
|
||||
});
|
||||
|
||||
// Agents are reloaded from the registry inside the constructor.
|
||||
// Re-call register() on each to ensure agent_directory blocks are current.
|
||||
for (const agent of swarm.getAll()) {
|
||||
swarm.register(agent);
|
||||
}
|
||||
|
||||
return swarm;
|
||||
}
|
||||
|
||||
// ─── Internal Helpers ──────────────────────────────────────────────────────
|
||||
|
||||
_buildDirectory() {
|
||||
return this.getAll().map((a) => ({
|
||||
name: a.identity.name,
|
||||
role: a.identity.role,
|
||||
tags: a.identity.tags ?? [],
|
||||
agentId: a.identity.agentId,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
function _bodyToString(body, fallback) {
|
||||
if (body == null) return fallback;
|
||||
return typeof body === "string" ? body : JSON.stringify(body);
|
||||
}
|
||||
|
|
@ -91,8 +91,16 @@ 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,
|
||||
|
|
|
|||
364
src/resources/extensions/sf/uok/persistent-agent.js
Normal file
364
src/resources/extensions/sf/uok/persistent-agent.js
Normal file
|
|
@ -0,0 +1,364 @@
|
|||
/**
|
||||
* PersistentAgent — Persistent named agent with durable inbox, 3-tier memory, and UOK integration.
|
||||
*
|
||||
* Purpose: provide persistent agents as first-class citizens in the SF
|
||||
* swarm system. Each agent has a stable identity that survives restarts, a SQLite-backed
|
||||
* inbox, and three memory tiers (core blocks, recall, archival).
|
||||
*
|
||||
* Consumer: AgentSwarm orchestrator, swarm role agents (CoordinatorAgent, WorkerAgent etc),
|
||||
* and direct use in multi-agent dispatch flows.
|
||||
*/
|
||||
|
||||
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 { UokCoordinationStore } from "./coordination-store.js";
|
||||
import { MessageBus } from "./message-bus.js";
|
||||
|
||||
const VALID_ROLES = ["coordinator", "worker", "scout", "reviewer", "adversary", "architect"];
|
||||
const DEFAULT_ROLE = "worker";
|
||||
const DEFAULT_MAX_INBOX_SIZE = 200;
|
||||
const DEFAULT_REFRESH_INTERVAL_MS = 30_000;
|
||||
const DEFAULT_SEND_AND_WAIT_TIMEOUT_MS = 30_000;
|
||||
const DEFAULT_SEND_AND_WAIT_POLL_INTERVAL_MS = 500;
|
||||
|
||||
function ensureDbOpen(basePath) {
|
||||
const dir = sfRoot(basePath);
|
||||
mkdirSync(dir, { recursive: true });
|
||||
const dbPath = join(dir, "sf.db");
|
||||
if (!getDatabase()) {
|
||||
openDatabase(dbPath);
|
||||
}
|
||||
const db = getDatabase();
|
||||
if (!db) throw new Error(`PersistentAgent: failed to open database at ${dbPath}`);
|
||||
return db;
|
||||
}
|
||||
|
||||
/**
|
||||
* Persistent named agent with durable inbox, 3-tier memory, and UOK integration.
|
||||
*
|
||||
* Purpose: first-class swarm citizen with Letta-style identity, memory, and messaging.
|
||||
* Survives restarts via SQLite-backed identity and CoordinationStore memory.
|
||||
*
|
||||
* Consumer: AgentSwarm orchestrator and role agents (coordinator, worker, scout, etc).
|
||||
*/
|
||||
export class PersistentAgent {
|
||||
/**
|
||||
* Create or resume a named agent.
|
||||
*
|
||||
* Purpose: establish stable agent identity on first init and reload it on
|
||||
* subsequent runs, so agent state (memory blocks, archival, tags) persists
|
||||
* across process restarts without re-registration.
|
||||
*
|
||||
* Consumer: AgentSwarm when spawning or resuming swarm role agents.
|
||||
*
|
||||
* @param {string} basePath - project root used to locate `.sf/sf.db`
|
||||
* @param {object} options
|
||||
* @param {string} options.name - stable human-readable routing key (required)
|
||||
* @param {string} [options.role='worker'] - agent role
|
||||
* @param {string[]} [options.tags=[]] - routing metadata tags
|
||||
* @param {number} [options.maxInboxSize=200]
|
||||
* @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;
|
||||
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(", ")}`);
|
||||
|
||||
this._basePath = basePath;
|
||||
this._name = name;
|
||||
this._maxInboxSize = maxInboxSize;
|
||||
this._refreshIntervalMs = refreshIntervalMs;
|
||||
|
||||
const db = ensureDbOpen(basePath);
|
||||
this._store = new UokCoordinationStore(db);
|
||||
this._bus = new MessageBus(basePath, { maxInboxSize, refreshIntervalMs });
|
||||
|
||||
this._identity = this._loadOrCreateIdentity({ name, role, tags });
|
||||
this._inbox = this._bus.getInbox(`agent:${name}`);
|
||||
}
|
||||
|
||||
// ─── Identity ─────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Returns the agent's stable identity record.
|
||||
*
|
||||
* Purpose: provide callers a single authoritative snapshot of who this agent
|
||||
* is (id, name, role, tags) without exposing mutable internals.
|
||||
*
|
||||
* Consumer: AgentSwarm roster queries and routing logic.
|
||||
*/
|
||||
get identity() {
|
||||
return { ...this._identity };
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the agent's durable inbox instance.
|
||||
*
|
||||
* Purpose: expose the AgentInbox so callers can subscribe or integrate
|
||||
* with the inbox directly when the messaging helpers are insufficient.
|
||||
*
|
||||
* Consumer: AgentSwarm orchestrator and advanced swarm coordination flows.
|
||||
*/
|
||||
get inbox() {
|
||||
return this._inbox;
|
||||
}
|
||||
|
||||
_identityKey() {
|
||||
return `agent:${this._name}:identity`;
|
||||
}
|
||||
|
||||
_loadOrCreateIdentity({ name, role, tags }) {
|
||||
const existing = this._store.get(this._identityKey());
|
||||
if (existing) return existing;
|
||||
const identity = {
|
||||
agentId: `agent-${randomUUID()}`,
|
||||
name,
|
||||
role,
|
||||
tags,
|
||||
createdAt: new Date().toISOString(),
|
||||
};
|
||||
this._store.set(this._identityKey(), identity);
|
||||
return identity;
|
||||
}
|
||||
|
||||
// ─── Core Blocks (in-context memory) ──────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Set a named core memory block.
|
||||
*
|
||||
* Purpose: persist agent in-context slabs (persona, human profile, system
|
||||
* state) so they survive restarts and can be injected into prompts via
|
||||
* getContextBlocks().
|
||||
*
|
||||
* Consumer: agent setup code and AgentSwarm role initialization.
|
||||
*
|
||||
* @param {string} label - block label (e.g. "persona", "human")
|
||||
* @param {*} value - serializable block content
|
||||
* @param {number} [ttlMs] - optional TTL in milliseconds
|
||||
*/
|
||||
setCoreBlock(label, value, ttlMs) {
|
||||
const key = `agent:${this._name}:block:${label}`;
|
||||
this._store.set(key, value, ttlMs != null ? { ttlMs } : undefined);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a named core memory block.
|
||||
*
|
||||
* Purpose: load agent in-context state for prompt injection or decision logic.
|
||||
*
|
||||
* Consumer: getContextBlocks and agent reasoning loops.
|
||||
*
|
||||
* @param {string} label
|
||||
* @returns {*} stored value or null if absent/expired
|
||||
*/
|
||||
getCoreBlock(label) {
|
||||
return this._store.get(`agent:${this._name}:block:${label}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all core blocks as a formatted markdown string for prompt injection.
|
||||
*
|
||||
* Purpose: produce a ready-to-inject prompt section from all non-expired
|
||||
* core memory blocks so LLM callers don't need to know the block schema.
|
||||
*
|
||||
* Consumer: prompt builders in AgentSwarm dispatch and task execution flows.
|
||||
*
|
||||
* @returns {string} formatted markdown section
|
||||
*/
|
||||
getContextBlocks() {
|
||||
const prefix = `agent:${this._name}:block:`;
|
||||
const entries = this._store.entries(prefix);
|
||||
if (entries.length === 0) return "";
|
||||
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);
|
||||
lines.push(`**${label}**: ${rendered}`);
|
||||
}
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
// ─── Archival Memory (long-term KV) ───────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Store a value in archival memory.
|
||||
*
|
||||
* Purpose: persist long-term agent knowledge (learned facts, task history,
|
||||
* resource pointers) that outlives individual task runs.
|
||||
*
|
||||
* Consumer: agent reasoning loops when surfacing durable conclusions.
|
||||
*
|
||||
* @param {string} key
|
||||
* @param {*} value
|
||||
* @param {number} [ttlMs] - optional TTL in milliseconds
|
||||
*/
|
||||
remember(key, value, ttlMs) {
|
||||
const storeKey = `agent:${this._name}:archive:${key}`;
|
||||
this._store.set(storeKey, value, ttlMs != null ? { ttlMs } : undefined);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a value from archival memory.
|
||||
*
|
||||
* Purpose: load previously stored long-term knowledge for decision or context.
|
||||
*
|
||||
* Consumer: agent reasoning loops and task context assembly.
|
||||
*
|
||||
* @param {string} key
|
||||
* @returns {*} stored value or null if absent/expired
|
||||
*/
|
||||
recall(key) {
|
||||
return this._store.get(`agent:${this._name}:archive:${key}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a value from archival memory.
|
||||
*
|
||||
* Purpose: remove stale or invalidated archival knowledge to keep agent
|
||||
* context clean and prevent outdated facts from contaminating reasoning.
|
||||
*
|
||||
* Consumer: agent cleanup routines and explicit memory invalidation paths.
|
||||
*
|
||||
* @param {string} key
|
||||
*/
|
||||
forget(key) {
|
||||
this._store.delete(`agent:${this._name}:archive:${key}`);
|
||||
}
|
||||
|
||||
// ─── Recall (conversation history) ────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Return the last N inbox messages in chronological order.
|
||||
*
|
||||
* Purpose: provide agents with recent conversation context so they can
|
||||
* reason over message history without loading the full inbox.
|
||||
*
|
||||
* Consumer: agent prompt builders and response generation loops.
|
||||
*
|
||||
* @param {number} [limit=20]
|
||||
* @returns {object[]} messages sorted oldest-first
|
||||
*/
|
||||
getRecall(limit = 20) {
|
||||
const all = this._inbox.list(false);
|
||||
return all.slice(-limit);
|
||||
}
|
||||
|
||||
// ─── Messaging ────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Send a message to another agent by name.
|
||||
*
|
||||
* Purpose: route a message to a named agent's durable inbox so it is
|
||||
* delivered even if the recipient is not currently running.
|
||||
*
|
||||
* Consumer: agent coordination flows and swarm task delegation.
|
||||
*
|
||||
* @param {string} toAgentName - recipient agent name (not agentId)
|
||||
* @param {*} body
|
||||
* @param {object} [metadata={}]
|
||||
* @returns {string} message ID
|
||||
*/
|
||||
send(toAgentName, body, metadata = {}) {
|
||||
return this._bus.send(
|
||||
`agent:${this._name}`,
|
||||
`agent:${toAgentName}`,
|
||||
body,
|
||||
metadata,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a message and wait for a reply on this agent's inbox.
|
||||
*
|
||||
* Purpose: implement synchronous request-reply across agent boundaries
|
||||
* without coupling sender and receiver to a shared call stack. The reply
|
||||
* is identified by metadata.replyTo matching the sent message ID.
|
||||
*
|
||||
* Consumer: coordinator-to-worker task delegation where a result is
|
||||
* needed before the coordinator can proceed.
|
||||
*
|
||||
* @param {string} toAgentName
|
||||
* @param {*} body
|
||||
* @param {object} [opts]
|
||||
* @param {number} [opts.timeoutMs=30000]
|
||||
* @param {number} [opts.pollIntervalMs=500]
|
||||
* @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 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,
|
||||
);
|
||||
if (reply) {
|
||||
clearInterval(interval);
|
||||
resolve(reply.body);
|
||||
return;
|
||||
}
|
||||
if (Date.now() >= deadline) {
|
||||
clearInterval(interval);
|
||||
reject(new Error(`PersistentAgent.sendAndWait: timeout after ${timeoutMs}ms waiting for reply to ${sentId}`));
|
||||
}
|
||||
}, pollIntervalMs);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Receive messages from this agent's inbox.
|
||||
*
|
||||
* Purpose: drain incoming messages for agent processing, with an option to
|
||||
* filter to unread-only to avoid reprocessing.
|
||||
*
|
||||
* Consumer: agent polling loops and event-driven dispatch handlers.
|
||||
*
|
||||
* @param {boolean} [unreadOnly=true]
|
||||
* @returns {object[]}
|
||||
*/
|
||||
receive(unreadOnly = true) {
|
||||
return this._inbox.list(unreadOnly);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark a message as read in this agent's inbox.
|
||||
*
|
||||
* Purpose: advance the inbox read cursor so subsequent receive(true) calls
|
||||
* do not re-deliver already-processed messages.
|
||||
*
|
||||
* Consumer: agent processing loops after handling each message.
|
||||
*
|
||||
* @param {string} messageId
|
||||
*/
|
||||
markRead(messageId) {
|
||||
this._inbox.markRead(messageId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcast a message to multiple agents by name.
|
||||
*
|
||||
* Purpose: fan-out a notification or task to a set of agents in one call
|
||||
* without the caller managing individual send loops.
|
||||
*
|
||||
* Consumer: coordinator agents distributing work to worker pools.
|
||||
*
|
||||
* @param {*} body
|
||||
* @param {object} metadata
|
||||
* @param {string[]} recipientNames - agent names (not IDs)
|
||||
* @returns {string[]} message IDs
|
||||
*/
|
||||
broadcast(body, metadata, recipientNames) {
|
||||
return this._bus.broadcast(
|
||||
`agent:${this._name}`,
|
||||
recipientNames.map((n) => `agent:${n}`),
|
||||
body,
|
||||
metadata,
|
||||
);
|
||||
}
|
||||
}
|
||||
201
src/resources/extensions/sf/uok/swarm-dispatch.js
Normal file
201
src/resources/extensions/sf/uok/swarm-dispatch.js
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
/**
|
||||
* swarm-dispatch.js — SwarmDispatchLayer routing UOK DispatchEnvelopes through the agent swarm.
|
||||
*
|
||||
* Purpose: bridge the UOK dispatch model (envelopes with unitType, workMode, unitId) to the
|
||||
* swarm agent topology, routing each envelope to the appropriate role agent and returning
|
||||
* a structured result. Provides cached swarm access so the same AgentSwarm instance is
|
||||
* reused across dispatch calls within a process.
|
||||
*
|
||||
* Consumer: UOK kernel dispatch path, parallel orchestrators, and /sf autonomous controller
|
||||
* when SF_A2A_ENABLED is set.
|
||||
*/
|
||||
|
||||
import { AgentSwarm } from "./agent-swarm.js";
|
||||
import { MessageBus } from "./message-bus.js";
|
||||
import { createDefaultSwarm } from "./swarm-roles.js";
|
||||
|
||||
// Module-level cache keyed by `${basePath}:${swarmName}`
|
||||
const _cache = new Map();
|
||||
|
||||
/**
|
||||
* @typedef {object} DispatchEnvelope
|
||||
* @property {string} unitId
|
||||
* @property {string} unitType - 'task' | 'slice' | 'milestone' | 'eval' | 'research'
|
||||
* @property {string} workMode - 'build' | 'repair' | 'review' | 'research' | 'coordinate'
|
||||
* @property {object} payload - arbitrary task data
|
||||
* @property {number} priority - 0-10
|
||||
* @property {string} scope - project scope string
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {object} DispatchResult
|
||||
* @property {string} messageId
|
||||
* @property {string} targetAgent
|
||||
* @property {string} swarmName
|
||||
* @property {DispatchEnvelope} envelope
|
||||
*/
|
||||
|
||||
/**
|
||||
* SwarmDispatchLayer — routes DispatchEnvelopes through the agent swarm topology.
|
||||
*
|
||||
* Purpose: bridge the UOK dispatch model to the swarm agent topology, routing each
|
||||
* envelope to the appropriate role agent and returning a structured result.
|
||||
*
|
||||
* Consumer: UOK kernel dispatch path, parallel orchestrators, and /sf autonomous
|
||||
* controller when SF_A2A_ENABLED is set.
|
||||
*/
|
||||
export class SwarmDispatchLayer {
|
||||
/**
|
||||
* @param {string} basePath - project root used to locate `.sf/sf.db`
|
||||
* @param {object} [options={}]
|
||||
* @param {string} [options.swarmName='default']
|
||||
* @param {boolean} [options.autoCreate=true]
|
||||
*/
|
||||
constructor(basePath, options = {}) {
|
||||
this._basePath = basePath;
|
||||
this._swarmName = options.swarmName ?? "default";
|
||||
this._autoCreate = options.autoCreate !== false;
|
||||
/** @type {AgentSwarm | null} */
|
||||
this._swarm = null;
|
||||
this._bus = new MessageBus(basePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the cached AgentSwarm, loading or creating it as needed.
|
||||
*
|
||||
* Purpose: ensure a single AgentSwarm instance is reused across dispatch
|
||||
* calls within a process to avoid redundant DB round-trips and agent
|
||||
* re-registration on every envelope.
|
||||
*
|
||||
* Consumer: dispatch() and dispatchBatch() before routing envelopes.
|
||||
*
|
||||
* @returns {Promise<AgentSwarm>}
|
||||
*/
|
||||
async getOrCreateSwarm() {
|
||||
if (this._swarm) return this._swarm;
|
||||
|
||||
let swarm = AgentSwarm.load(this._basePath, this._swarmName);
|
||||
|
||||
// load() returns a new empty swarm when no saved registry exists.
|
||||
// If it has no agents and autoCreate is on, bootstrap a default topology.
|
||||
if (swarm.getAll().length === 0 && this._autoCreate) {
|
||||
const result = await createDefaultSwarm(this._basePath, {
|
||||
swarmName: this._swarmName,
|
||||
});
|
||||
swarm = result.swarm;
|
||||
}
|
||||
|
||||
this._swarm = swarm;
|
||||
return this._swarm;
|
||||
}
|
||||
|
||||
/**
|
||||
* Route a single DispatchEnvelope to the appropriate swarm agent.
|
||||
*
|
||||
* Purpose: select the right role agent for the envelope's workMode/unitType,
|
||||
* deliver the payload to that agent's durable inbox, and return a structured
|
||||
* result so callers can track message delivery without knowing the swarm topology.
|
||||
*
|
||||
* Consumer: UOK kernel dispatch path and swarmDispatch() convenience function.
|
||||
*
|
||||
* @param {DispatchEnvelope} envelope
|
||||
* @returns {Promise<DispatchResult>}
|
||||
*/
|
||||
async dispatch(envelope) {
|
||||
const swarm = await this.getOrCreateSwarm();
|
||||
const target = swarm.route(envelope);
|
||||
|
||||
if (!target) {
|
||||
throw new Error(
|
||||
`SwarmDispatchLayer: no agent available to handle envelope unitType=${envelope.unitType} workMode=${envelope.workMode}`,
|
||||
);
|
||||
}
|
||||
|
||||
const from = `dispatch:${envelope.scope}:${envelope.unitId}`;
|
||||
const to = `agent:${target.identity.name}`;
|
||||
const metadata = {
|
||||
unitId: envelope.unitId,
|
||||
unitType: envelope.unitType,
|
||||
workMode: envelope.workMode,
|
||||
};
|
||||
|
||||
const messageId = this._bus.send(from, to, envelope.payload, metadata);
|
||||
|
||||
return {
|
||||
messageId,
|
||||
targetAgent: target.identity.name,
|
||||
swarmName: this._swarmName,
|
||||
envelope,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Route multiple DispatchEnvelopes through the swarm in parallel.
|
||||
*
|
||||
* Purpose: fan-out a batch of work units to the swarm concurrently so
|
||||
* throughput scales with the number of envelopes rather than serializing them.
|
||||
*
|
||||
* Consumer: parallel orchestrators and autonomous dispatch batching loops.
|
||||
*
|
||||
* @param {DispatchEnvelope[]} envelopes
|
||||
* @returns {Promise<DispatchResult[]>}
|
||||
*/
|
||||
async dispatchBatch(envelopes) {
|
||||
return Promise.all(envelopes.map((envelope) => this.dispatch(envelope)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a structured view of the swarm topology by role.
|
||||
*
|
||||
* Purpose: expose the swarm role snapshot so callers can inspect routing
|
||||
* capacity without reaching into the AgentSwarm directly.
|
||||
*
|
||||
* Consumer: diagnostic tooling, health checks, and /sf swarm status.
|
||||
*
|
||||
* @returns {Promise<{ coordinator: import('./persistent-agent.js').PersistentAgent[], workers: import('./persistent-agent.js').PersistentAgent[], scouts: import('./persistent-agent.js').PersistentAgent[], reviewers: import('./persistent-agent.js').PersistentAgent[], all: import('./persistent-agent.js').PersistentAgent[] }>}
|
||||
*/
|
||||
async getTopology() {
|
||||
const swarm = await this.getOrCreateSwarm();
|
||||
return swarm.getTopology();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return or create a SwarmDispatchLayer for the given basePath and swarmName.
|
||||
*
|
||||
* Purpose: provide a process-level singleton factory so callers that don't
|
||||
* manage a SwarmDispatchLayer instance directly share the same cached swarm
|
||||
* and avoid redundant initialization on repeated dispatch calls.
|
||||
*
|
||||
* Consumer: swarmDispatch() convenience function and any caller that wants
|
||||
* a default dispatch layer without explicit lifecycle management.
|
||||
*
|
||||
* @param {string} basePath
|
||||
* @param {object} [options={}]
|
||||
* @param {string} [options.swarmName='default']
|
||||
* @param {boolean} [options.autoCreate=true]
|
||||
* @returns {SwarmDispatchLayer}
|
||||
*/
|
||||
static getOrCreate(basePath, options = {}) {
|
||||
const key = `${basePath}:${options.swarmName ?? "default"}`;
|
||||
if (!_cache.has(key)) {
|
||||
_cache.set(key, new SwarmDispatchLayer(basePath, options));
|
||||
}
|
||||
return _cache.get(key);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Route a single DispatchEnvelope through the default swarm for basePath.
|
||||
*
|
||||
* Purpose: one-liner entry point for dispatch callers that don't need to manage
|
||||
* a SwarmDispatchLayer instance directly.
|
||||
*
|
||||
* Consumer: UOK kernel dispatch integration, test helpers.
|
||||
*
|
||||
* @param {string} basePath
|
||||
* @param {DispatchEnvelope} envelope
|
||||
* @returns {Promise<DispatchResult>}
|
||||
*/
|
||||
export async function swarmDispatch(basePath, envelope) {
|
||||
return SwarmDispatchLayer.getOrCreate(basePath).dispatch(envelope);
|
||||
}
|
||||
196
src/resources/extensions/sf/uok/swarm-roles.js
Normal file
196
src/resources/extensions/sf/uok/swarm-roles.js
Normal file
|
|
@ -0,0 +1,196 @@
|
|||
/**
|
||||
* 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.
|
||||
*
|
||||
* 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";
|
||||
|
||||
/**
|
||||
* Coordinator agent — owns routing and task dispatch across a worker pool.
|
||||
*
|
||||
* Purpose: provide a persistent orchestrator that routes incoming work to
|
||||
* the right worker based on message workMode, accumulates results, and
|
||||
* maintains swarm-level state in its core blocks.
|
||||
*
|
||||
* Consumer: createDefaultSwarm, SwarmDispatchLayer, and /sf swarm command.
|
||||
*/
|
||||
export class CoordinatorAgent extends PersistentAgent {
|
||||
/**
|
||||
* @param {string} basePath
|
||||
* @param {object} [options={}]
|
||||
* @param {string} [options.name='coordinator']
|
||||
* @param {string[]} [options.tags=[]]
|
||||
*/
|
||||
constructor(basePath, options = {}) {
|
||||
super(basePath, {
|
||||
name: options.name ?? "coordinator",
|
||||
role: "coordinator",
|
||||
tags: ["role:coordinator", "tier:persistent", ...(options.tags ?? [])],
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Route an incoming message body to the right worker by reading its workMode field.
|
||||
*
|
||||
* Purpose: select a suitable worker from the swarm roster and forward the
|
||||
* message so the coordinator does not block on task execution. Selection
|
||||
* prefers agents tagged with `workMode:<value>`, falling back to any
|
||||
* available worker-tier agent.
|
||||
*
|
||||
* Consumer: autonomous dispatch loops and /sf swarm run command.
|
||||
*
|
||||
* @param {AgentSwarm} swarm - the swarm containing candidate workers
|
||||
* @param {object} messageBody - message body; `messageBody.workMode` guides selection
|
||||
* @param {object} [metadata={}] - forwarded as message metadata
|
||||
* @returns {{ targetAgent: PersistentAgent, messageId: string } | null}
|
||||
*/
|
||||
routeToWorker(swarm, messageBody, metadata = {}) {
|
||||
const { workMode } = messageBody ?? {};
|
||||
const candidates = swarm.agents.filter((a) => {
|
||||
const id = a.identity;
|
||||
if (id.name === this._name) return false;
|
||||
if (!id.tags.includes("tier:worker")) return false;
|
||||
if (workMode && id.tags.includes(`workMode:${workMode}`)) return true;
|
||||
return !workMode;
|
||||
});
|
||||
|
||||
// Prefer specialised match; fall back to first available worker.
|
||||
const specialised = workMode
|
||||
? candidates.filter((a) => a.identity.tags.includes(`workMode:${workMode}`))
|
||||
: [];
|
||||
const target = specialised[0] ?? candidates[0] ?? null;
|
||||
|
||||
if (!target) return null;
|
||||
|
||||
const messageId = this.send(target.identity.name, messageBody, metadata);
|
||||
return { targetAgent: target, messageId };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Worker agent — executes assigned tasks in the swarm.
|
||||
*
|
||||
* Purpose: provide a general-purpose execution agent that accepts task
|
||||
* messages from a coordinator and runs them to completion.
|
||||
*
|
||||
* 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",
|
||||
role: "worker",
|
||||
tags: ["role:worker", "tier:worker", ...(options.tags ?? [])],
|
||||
...options,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* 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",
|
||||
role: "scout",
|
||||
tags: ["role:scout", "tier:worker", ...(options.tags ?? [])],
|
||||
...options,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reviewer agent — validates and critiques worker output.
|
||||
*
|
||||
* Purpose: provide a dedicated quality-gate agent that receives completed
|
||||
* task output from workers and either approves, requests revision, or
|
||||
* escalates to the coordinator.
|
||||
*
|
||||
* Consumer: createDefaultSwarm and any dispatch flow that requires 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",
|
||||
role: "reviewer",
|
||||
tags: ["role:reviewer", "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.
|
||||
*
|
||||
* Consumer: SwarmDispatchLayer.getOrCreateSwarm() and direct initialization in
|
||||
* 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 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 swarm = new AgentSwarm(basePath, {
|
||||
name: options.swarmName ?? "default",
|
||||
managerType: options.managerType ?? ManagerType.SUPERVISOR,
|
||||
});
|
||||
|
||||
swarm.register(coordinator);
|
||||
for (const worker of workers) {
|
||||
swarm.register(worker);
|
||||
}
|
||||
swarm.register(scout);
|
||||
swarm.register(reviewer);
|
||||
|
||||
swarm.persist();
|
||||
|
||||
return { swarm, coordinator, workers, scout, reviewer };
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@
|
|||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"incremental": false,
|
||||
"types": ["node"],
|
||||
"rootDir": "src/resources",
|
||||
"outDir": "dist/resources",
|
||||
"declaration": false,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue