Merge commit '4ceff0ecf9d6668c894acb202410de67e44271c6'
This commit is contained in:
12
agents/base-agent.template.json
Normal file
12
agents/base-agent.template.json
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"id": "base-agent",
|
||||||
|
"name": "Unnamed Agent",
|
||||||
|
"role": "worker",
|
||||||
|
"traits": [],
|
||||||
|
"inputs": [],
|
||||||
|
"outputs": [],
|
||||||
|
"description": "This is a template agent used for cloning new agent definitions.",
|
||||||
|
"triggers": [],
|
||||||
|
"inherits_from": null,
|
||||||
|
"active": true
|
||||||
|
}
|
||||||
11
agents/broadcast-agent.json
Normal file
11
agents/broadcast-agent.json
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"id": "broadcast-agent",
|
||||||
|
"name": "Broadcast Agent",
|
||||||
|
"role": "notifier",
|
||||||
|
"traits": ["slack-poster", "digest-broadcaster", "cross-platform-relay"],
|
||||||
|
"inputs": ["digest-summary", "status-bar"],
|
||||||
|
"outputs": ["slack-post", "webhook-ping"],
|
||||||
|
"description": "Delivers updates across Discord, Slack, and BlackRoad Console views.",
|
||||||
|
"triggers": ["digest-complete", "reaction::📣"],
|
||||||
|
"inherits_from": "base-agent"
|
||||||
|
}
|
||||||
11
agents/guardian-agent.json
Normal file
11
agents/guardian-agent.json
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"id": "guardian-agent",
|
||||||
|
"name": "Guardian Agent",
|
||||||
|
"role": "escalation-monitor",
|
||||||
|
"traits": ["security-alert", "incident-response", "risk-dashboard"],
|
||||||
|
"inputs": ["reaction::🛟", "field::blocked"],
|
||||||
|
"outputs": ["alert.md", "assign-to-human"],
|
||||||
|
"description": "Monitors system health, escalations, and redirects blocked issues to human triage.",
|
||||||
|
"triggers": ["reaction::🛟", "status::blocked"],
|
||||||
|
"inherits_from": "base-agent"
|
||||||
|
}
|
||||||
11
agents/planner-agent.json
Normal file
11
agents/planner-agent.json
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"id": "planner-agent",
|
||||||
|
"name": "Planner Agent",
|
||||||
|
"role": "timeline-scope",
|
||||||
|
"traits": ["sprint-forecast", "priority-assessor", "load-balancer"],
|
||||||
|
"inputs": ["project-field", "weekly-digest"],
|
||||||
|
"outputs": ["sprint-plan.md", "effort-estimate.txt"],
|
||||||
|
"description": "Analyzes scope, assigns sprints, and calculates agent load capacity.",
|
||||||
|
"triggers": ["cron::weekly", "reaction::📅"],
|
||||||
|
"inherits_from": "base-agent"
|
||||||
|
}
|
||||||
11
agents/qa-agent.json
Normal file
11
agents/qa-agent.json
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"id": "qa-agent",
|
||||||
|
"name": "Quality Assurance Agent",
|
||||||
|
"role": "verifier",
|
||||||
|
"traits": ["test-runner", "assertion-logic", "workflow-tester"],
|
||||||
|
"inputs": ["pull-request", "build-log"],
|
||||||
|
"outputs": ["test-report.md", "bug-report.md"],
|
||||||
|
"description": "Runs sanity checks, integration tests, and validates emoji workflows.",
|
||||||
|
"triggers": ["pr-opened", "reaction::🔁"],
|
||||||
|
"inherits_from": "base-agent"
|
||||||
|
}
|
||||||
11
agents/scribe-agent.json
Normal file
11
agents/scribe-agent.json
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"id": "scribe-agent",
|
||||||
|
"name": "Scribe Agent",
|
||||||
|
"role": "documentation",
|
||||||
|
"traits": ["markdown-writer", "summary-generator", "spec-author"],
|
||||||
|
"inputs": ["issue-comment", "status-update"],
|
||||||
|
"outputs": ["README.md", "docs/*.md"],
|
||||||
|
"description": "Generates Markdown documentation from project activity and agent workflows.",
|
||||||
|
"triggers": ["reaction::✍🏼", "status::in-progress"],
|
||||||
|
"inherits_from": "base-agent"
|
||||||
|
}
|
||||||
@@ -10,3 +10,5 @@
|
|||||||
export * from "./types";
|
export * from "./types";
|
||||||
export { spawnAgent, saveAgent, main as spawnAgentCli } from "./spawn-agent";
|
export { spawnAgent, saveAgent, main as spawnAgentCli } from "./spawn-agent";
|
||||||
export { AgentBuilder, AgentRegistry, createAgent, getRegistry } from "./lucidia-agent-builder";
|
export { AgentBuilder, AgentRegistry, createAgent, getRegistry } from "./lucidia-agent-builder";
|
||||||
|
export * from "./types";
|
||||||
|
export * from "./loader";
|
||||||
|
|||||||
135
src/agents/loader.ts
Normal file
135
src/agents/loader.ts
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
import * as fs from "fs";
|
||||||
|
import * as path from "path";
|
||||||
|
import type { Agent, AgentValidationResult } from "./types";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the agents directory path, supporting both source and compiled environments.
|
||||||
|
* Can be overridden via AGENTS_DIR environment variable.
|
||||||
|
*/
|
||||||
|
function getAgentsDir(): string {
|
||||||
|
if (process.env.AGENTS_DIR) {
|
||||||
|
return process.env.AGENTS_DIR;
|
||||||
|
}
|
||||||
|
// Try to find agents directory relative to project root
|
||||||
|
const projectRoot = path.resolve(__dirname, "../..");
|
||||||
|
return path.join(projectRoot, "agents");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates an agent object against the expected schema
|
||||||
|
*/
|
||||||
|
export function validateAgent(agent: unknown): AgentValidationResult {
|
||||||
|
const errors: string[] = [];
|
||||||
|
|
||||||
|
if (typeof agent !== "object" || agent === null) {
|
||||||
|
return { valid: false, errors: ["Agent must be an object"] };
|
||||||
|
}
|
||||||
|
|
||||||
|
const obj = agent as Record<string, unknown>;
|
||||||
|
|
||||||
|
if (typeof obj.id !== "string" || obj.id.length === 0) {
|
||||||
|
errors.push("Agent must have a non-empty string 'id'");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof obj.name !== "string" || obj.name.length === 0) {
|
||||||
|
errors.push("Agent must have a non-empty string 'name'");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof obj.role !== "string" || obj.role.length === 0) {
|
||||||
|
errors.push("Agent must have a non-empty string 'role'");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Array.isArray(obj.traits)) {
|
||||||
|
errors.push("Agent must have an array 'traits'");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Array.isArray(obj.inputs)) {
|
||||||
|
errors.push("Agent must have an array 'inputs'");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Array.isArray(obj.outputs)) {
|
||||||
|
errors.push("Agent must have an array 'outputs'");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof obj.description !== "string") {
|
||||||
|
errors.push("Agent must have a string 'description'");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Array.isArray(obj.triggers)) {
|
||||||
|
errors.push("Agent must have an array 'triggers'");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (obj.inherits_from !== null && typeof obj.inherits_from !== "string") {
|
||||||
|
errors.push("Agent 'inherits_from' must be null or a string");
|
||||||
|
}
|
||||||
|
|
||||||
|
return { valid: errors.length === 0, errors };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads an agent from a JSON file
|
||||||
|
*/
|
||||||
|
export function loadAgent(filename: string): Agent {
|
||||||
|
const agentsDir = getAgentsDir();
|
||||||
|
const filepath = path.join(agentsDir, filename);
|
||||||
|
const content = fs.readFileSync(filepath, "utf8");
|
||||||
|
const agent = JSON.parse(content);
|
||||||
|
|
||||||
|
const validation = validateAgent(agent);
|
||||||
|
if (!validation.valid) {
|
||||||
|
throw new Error(`Invalid agent '${filename}': ${validation.errors.join(", ")}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return agent as Agent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads all agent definitions from the agents directory
|
||||||
|
*/
|
||||||
|
export function loadAllAgents(): Agent[] {
|
||||||
|
const agentsDir = getAgentsDir();
|
||||||
|
const files = fs.readdirSync(agentsDir).filter((f) => f.endsWith(".json"));
|
||||||
|
return files.map((file) => loadAgent(file));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the base agent template
|
||||||
|
*/
|
||||||
|
export function getBaseTemplate(): Agent {
|
||||||
|
return loadAgent("base-agent.template.json");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new agent from the base template
|
||||||
|
*/
|
||||||
|
export function createAgentFromTemplate(
|
||||||
|
id: string,
|
||||||
|
name: string,
|
||||||
|
role: string,
|
||||||
|
overrides: Partial<Agent> = {}
|
||||||
|
): Agent {
|
||||||
|
const base = getBaseTemplate();
|
||||||
|
return {
|
||||||
|
...base,
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
role,
|
||||||
|
...overrides,
|
||||||
|
inherits_from: "base-agent",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets an agent by its ID
|
||||||
|
*/
|
||||||
|
export function getAgentById(id: string): Agent | undefined {
|
||||||
|
const agents = loadAllAgents();
|
||||||
|
return agents.find((agent) => agent.id === id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lists all available agent IDs
|
||||||
|
*/
|
||||||
|
export function listAgentIds(): string[] {
|
||||||
|
return loadAllAgents().map((agent) => agent.id);
|
||||||
|
}
|
||||||
@@ -26,6 +26,8 @@ export interface AgentMetadata {
|
|||||||
lastModified?: string;
|
lastModified?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
* Agent type definition for BlackRoad OS Genesis Agents
|
||||||
|
*/
|
||||||
export interface Agent {
|
export interface Agent {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
@@ -55,3 +57,19 @@ export type AgentRole =
|
|||||||
| "archive"
|
| "archive"
|
||||||
| "support"
|
| "support"
|
||||||
| "custom";
|
| "custom";
|
||||||
|
traits: string[];
|
||||||
|
inputs: string[];
|
||||||
|
outputs: string[];
|
||||||
|
description: string;
|
||||||
|
triggers: string[];
|
||||||
|
inherits_from: string | null;
|
||||||
|
active?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Agent validation result
|
||||||
|
*/
|
||||||
|
export interface AgentValidationResult {
|
||||||
|
valid: boolean;
|
||||||
|
errors: string[];
|
||||||
|
}
|
||||||
|
|||||||
171
tests/agents.test.ts
Normal file
171
tests/agents.test.ts
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import {
|
||||||
|
validateAgent,
|
||||||
|
loadAgent,
|
||||||
|
loadAllAgents,
|
||||||
|
getBaseTemplate,
|
||||||
|
createAgentFromTemplate,
|
||||||
|
getAgentById,
|
||||||
|
listAgentIds,
|
||||||
|
} from "../src/agents";
|
||||||
|
|
||||||
|
describe("Agent Loader", () => {
|
||||||
|
describe("validateAgent", () => {
|
||||||
|
it("validates a correct agent", () => {
|
||||||
|
const agent = {
|
||||||
|
id: "test-agent",
|
||||||
|
name: "Test Agent",
|
||||||
|
role: "worker",
|
||||||
|
traits: ["trait1"],
|
||||||
|
inputs: ["input1"],
|
||||||
|
outputs: ["output1"],
|
||||||
|
description: "A test agent",
|
||||||
|
triggers: ["trigger1"],
|
||||||
|
inherits_from: null,
|
||||||
|
};
|
||||||
|
const result = validateAgent(agent);
|
||||||
|
expect(result.valid).toBe(true);
|
||||||
|
expect(result.errors).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects non-object agent", () => {
|
||||||
|
const result = validateAgent(null);
|
||||||
|
expect(result.valid).toBe(false);
|
||||||
|
expect(result.errors).toContain("Agent must be an object");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects agent with missing id", () => {
|
||||||
|
const agent = {
|
||||||
|
name: "Test",
|
||||||
|
role: "worker",
|
||||||
|
traits: [],
|
||||||
|
inputs: [],
|
||||||
|
outputs: [],
|
||||||
|
description: "",
|
||||||
|
triggers: [],
|
||||||
|
inherits_from: null,
|
||||||
|
};
|
||||||
|
const result = validateAgent(agent);
|
||||||
|
expect(result.valid).toBe(false);
|
||||||
|
expect(result.errors).toContain("Agent must have a non-empty string 'id'");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects agent with non-array traits", () => {
|
||||||
|
const agent = {
|
||||||
|
id: "test",
|
||||||
|
name: "Test",
|
||||||
|
role: "worker",
|
||||||
|
traits: "not-an-array",
|
||||||
|
inputs: [],
|
||||||
|
outputs: [],
|
||||||
|
description: "",
|
||||||
|
triggers: [],
|
||||||
|
inherits_from: null,
|
||||||
|
};
|
||||||
|
const result = validateAgent(agent);
|
||||||
|
expect(result.valid).toBe(false);
|
||||||
|
expect(result.errors).toContain("Agent must have an array 'traits'");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("loadAgent", () => {
|
||||||
|
it("loads the base agent template", () => {
|
||||||
|
const base = loadAgent("base-agent.template.json");
|
||||||
|
expect(base.id).toBe("base-agent");
|
||||||
|
expect(base.name).toBe("Unnamed Agent");
|
||||||
|
expect(base.role).toBe("worker");
|
||||||
|
expect(base.active).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("loads scribe-agent", () => {
|
||||||
|
const agent = loadAgent("scribe-agent.json");
|
||||||
|
expect(agent.id).toBe("scribe-agent");
|
||||||
|
expect(agent.role).toBe("documentation");
|
||||||
|
expect(agent.inherits_from).toBe("base-agent");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("loads planner-agent", () => {
|
||||||
|
const agent = loadAgent("planner-agent.json");
|
||||||
|
expect(agent.id).toBe("planner-agent");
|
||||||
|
expect(agent.role).toBe("timeline-scope");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("loads qa-agent", () => {
|
||||||
|
const agent = loadAgent("qa-agent.json");
|
||||||
|
expect(agent.id).toBe("qa-agent");
|
||||||
|
expect(agent.role).toBe("verifier");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("loads broadcast-agent", () => {
|
||||||
|
const agent = loadAgent("broadcast-agent.json");
|
||||||
|
expect(agent.id).toBe("broadcast-agent");
|
||||||
|
expect(agent.role).toBe("notifier");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("loads guardian-agent", () => {
|
||||||
|
const agent = loadAgent("guardian-agent.json");
|
||||||
|
expect(agent.id).toBe("guardian-agent");
|
||||||
|
expect(agent.role).toBe("escalation-monitor");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("loadAllAgents", () => {
|
||||||
|
it("loads all agent definitions", () => {
|
||||||
|
const agents = loadAllAgents();
|
||||||
|
expect(agents.length).toBeGreaterThanOrEqual(6);
|
||||||
|
const ids = agents.map((a) => a.id);
|
||||||
|
expect(ids).toContain("base-agent");
|
||||||
|
expect(ids).toContain("scribe-agent");
|
||||||
|
expect(ids).toContain("planner-agent");
|
||||||
|
expect(ids).toContain("qa-agent");
|
||||||
|
expect(ids).toContain("broadcast-agent");
|
||||||
|
expect(ids).toContain("guardian-agent");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getBaseTemplate", () => {
|
||||||
|
it("returns the base template", () => {
|
||||||
|
const base = getBaseTemplate();
|
||||||
|
expect(base.id).toBe("base-agent");
|
||||||
|
expect(base.inherits_from).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("createAgentFromTemplate", () => {
|
||||||
|
it("creates a new agent from base template", () => {
|
||||||
|
const newAgent = createAgentFromTemplate(
|
||||||
|
"custom-agent",
|
||||||
|
"Custom Agent",
|
||||||
|
"custom-role",
|
||||||
|
{ traits: ["custom-trait"], description: "A custom agent" }
|
||||||
|
);
|
||||||
|
expect(newAgent.id).toBe("custom-agent");
|
||||||
|
expect(newAgent.name).toBe("Custom Agent");
|
||||||
|
expect(newAgent.role).toBe("custom-role");
|
||||||
|
expect(newAgent.traits).toEqual(["custom-trait"]);
|
||||||
|
expect(newAgent.inherits_from).toBe("base-agent");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getAgentById", () => {
|
||||||
|
it("finds agent by id", () => {
|
||||||
|
const agent = getAgentById("scribe-agent");
|
||||||
|
expect(agent).toBeDefined();
|
||||||
|
expect(agent?.name).toBe("Scribe Agent");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns undefined for unknown id", () => {
|
||||||
|
const agent = getAgentById("unknown-agent");
|
||||||
|
expect(agent).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("listAgentIds", () => {
|
||||||
|
it("lists all agent ids", () => {
|
||||||
|
const ids = listAgentIds();
|
||||||
|
expect(ids).toContain("base-agent");
|
||||||
|
expect(ids).toContain("scribe-agent");
|
||||||
|
expect(ids).toContain("guardian-agent");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user