fill ASI gaps: agent observability, specialization optimizer, message bus
This commit is contained in:
parent
4d9628db8b
commit
3fd9758da3
|
|
@ -0,0 +1,62 @@
|
|||
import { AgentObservability } from "../upgrades/agent-observability.js";
|
||||
import { AgentSpecialization } from "../upgrades/agent-specialization.js";
|
||||
import { AgentMessageBus } from "../upgrades/agent-message-bus.js";
|
||||
|
||||
function log(msg: string): void { process.stdout.write(msg + "\n"); }
|
||||
|
||||
function main(): void {
|
||||
let passed = 0;
|
||||
let total = 0;
|
||||
|
||||
// 1. Agent Observability
|
||||
log("=== Agent Observability ===");
|
||||
const obs = new AgentObservability();
|
||||
for (let i = 0; i < 8; i++) {
|
||||
obs.record({
|
||||
timestamp: new Date().toISOString(),
|
||||
agentId: `agent-${i % 3}`,
|
||||
agentRole: i % 2 === 0 ? "verifier" : "maker",
|
||||
eventType: "verdict",
|
||||
details: i < 6 ? "pass" : `result for task ${i}`,
|
||||
relatedAgentIds: [`agent-${(i + 1) % 3}`],
|
||||
});
|
||||
}
|
||||
const signals = obs.scan();
|
||||
total++; passed += signals.length > 0 ? 1 : 0;
|
||||
log(` Emergent signals detected: ${signals.length} ${signals.length > 0 ? "PASS" : "FAIL"}`);
|
||||
for (const s of signals) log(` ${s.type} (${s.severity})`);
|
||||
|
||||
// 2. Agent Specialization
|
||||
log("\n=== Agent Specialization ===");
|
||||
const spec = new AgentSpecialization();
|
||||
spec.record({ agentId: "sonnet-1", executorType: "sonnet-4-6", taskType: "code", success: true, quality: 0.9, durationMs: 5000 });
|
||||
spec.record({ agentId: "sonnet-1", executorType: "sonnet-4-6", taskType: "code", success: true, quality: 0.85, durationMs: 4500 });
|
||||
spec.record({ agentId: "haiku-1", executorType: "haiku", taskType: "verification", success: true, quality: 0.95, durationMs: 1000 });
|
||||
const route = spec.route("code");
|
||||
total++; passed += route.agentId === "sonnet-1" ? 1 : 0;
|
||||
log(` Code routing: ${route.agentId} ${route.agentId === "sonnet-1" ? "PASS" : "FAIL"}`);
|
||||
const report = spec.getSpecializationReport();
|
||||
total++; passed += report.includes("Code") ? 1 : 0;
|
||||
log(` Report generated: ${report.includes("code:") ? "PASS" : "FAIL"}`);
|
||||
|
||||
// 3. Agent Message Bus
|
||||
log("\n=== Agent Message Bus ===");
|
||||
const bus = new AgentMessageBus();
|
||||
const msg1 = bus.send({ fromAgent: "orchestrator", toAgent: "scout-1", type: "delegation", subject: "Scout codebase", body: "Explore the project structure" });
|
||||
const msg2 = bus.send({ fromAgent: "scout-1", toAgent: "orchestrator", type: "result", subject: "Scout complete", body: "Found 3 key files", parentMessageId: msg1.id });
|
||||
bus.raiseContradiction({ fromAgent: "verifier-1", toAgent: "maker-1", claim: "Uses JWT", counterClaim: "Uses session auth", evidence: ["Line 42: jwt.verify()", "Line 15: express-session"] });
|
||||
|
||||
total++; passed += bus.getInbox("scout-1").length > 0 ? 1 : 0;
|
||||
log(` Inbox for scout-1: ${bus.getInbox("scout-1").length} messages ${bus.getInbox("scout-1").length > 0 ? "PASS" : "FAIL"}`);
|
||||
total++; passed += bus.getThread(msg1.id).length === 2 ? 1 : 0;
|
||||
log(` Thread length: ${bus.getThread(msg1.id).length} ${bus.getThread(msg1.id).length === 2 ? "PASS" : "FAIL"}`);
|
||||
total++; passed += bus.getActiveContradictions().length > 0 ? 1 : 0;
|
||||
log(` Active contradictions: ${bus.getActiveContradictions().length} ${bus.getActiveContradictions().length > 0 ? "PASS" : "FAIL"}`);
|
||||
const synthesis = bus.synthesize(["scout-1"], "Project analysis");
|
||||
total++; passed += synthesis.includes("scout-1") ? 1 : 0;
|
||||
log(` Synthesis: ${synthesis.includes("scout-1") ? "PASS" : "FAIL"}`);
|
||||
|
||||
log(`\n=== ${passed}/${total} passed ===`);
|
||||
}
|
||||
|
||||
main();
|
||||
|
|
@ -0,0 +1,235 @@
|
|||
/**
|
||||
* Agent Message Bus — inter-agent communication protocol for multi-agent collectives.
|
||||
*
|
||||
* From arXiv:2606.12683 (Genewein et al.): "Multi-agent collectives require
|
||||
* communication protocols for agents to share findings, delegate subtasks,
|
||||
* and resolve contradictions."
|
||||
*
|
||||
* Previously, agents shared state through shared memory but had no direct
|
||||
* communication. This message bus adds:
|
||||
* 1. Direct messaging — agent A sends a message to agent B
|
||||
* 2. Broadcast — agent A sends a message to all agents of a type
|
||||
* 3. Delegation — agent A requests agent B to perform a subtask
|
||||
* 4. Contradiction resolution — two agents with conflicting outputs negotiate
|
||||
* 5. Results aggregation — collect and synthesize multiple agent outputs
|
||||
*
|
||||
* Messages are persisted, traceable, and auditable.
|
||||
*/
|
||||
|
||||
import { StateStore } from "../core/state-store.js";
|
||||
|
||||
export type MessagePriority = "low" | "medium" | "high" | "critical";
|
||||
export type MessageStatus = "sent" | "delivered" | "read" | "resolved" | "expired";
|
||||
|
||||
export interface AgentMessage {
|
||||
id: string;
|
||||
fromAgent: string;
|
||||
toAgent: string;
|
||||
type: "direct" | "broadcast" | "delegation" | "contradiction" | "result" | "synthesis";
|
||||
subject: string;
|
||||
body: string;
|
||||
priority: MessagePriority;
|
||||
status: MessageStatus;
|
||||
parentMessageId?: string;
|
||||
contextIds?: string[];
|
||||
createdAt: string;
|
||||
deliveredAt?: string;
|
||||
resolvedAt?: string;
|
||||
}
|
||||
|
||||
export interface MessageThread {
|
||||
messages: AgentMessage[];
|
||||
subject: string;
|
||||
participants: string[];
|
||||
status: "active" | "resolved" | "stale";
|
||||
}
|
||||
|
||||
export class AgentMessageBus {
|
||||
private store: StateStore;
|
||||
private messages: AgentMessage[] = [];
|
||||
private subscriptions: Map<string, Array<(msg: AgentMessage) => void>> = new Map();
|
||||
|
||||
constructor(store?: StateStore) {
|
||||
this.store = store ?? new StateStore();
|
||||
this.store.ensureSubDir("messages");
|
||||
this.load();
|
||||
}
|
||||
|
||||
/** Send a message */
|
||||
send(params: {
|
||||
fromAgent: string;
|
||||
toAgent: string;
|
||||
type: AgentMessage["type"];
|
||||
subject: string;
|
||||
body: string;
|
||||
priority?: MessagePriority;
|
||||
parentMessageId?: string;
|
||||
}): AgentMessage {
|
||||
const msg: AgentMessage = {
|
||||
id: StateStore.uid().slice(0, 12),
|
||||
fromAgent: params.fromAgent,
|
||||
toAgent: params.toAgent,
|
||||
type: params.type,
|
||||
subject: params.subject,
|
||||
body: params.body,
|
||||
priority: params.priority ?? "medium",
|
||||
status: "sent",
|
||||
parentMessageId: params.parentMessageId,
|
||||
createdAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
this.messages.push(msg);
|
||||
this.store.append("messages", `inbox-${params.toAgent}.jsonl`, msg);
|
||||
|
||||
// Notify subscribers
|
||||
const subscribers = this.subscriptions.get(params.toAgent);
|
||||
if (subscribers) {
|
||||
for (const sub of subscribers) sub(msg);
|
||||
}
|
||||
|
||||
return msg;
|
||||
}
|
||||
|
||||
/** Broadcast to all agents of a type */
|
||||
broadcast(params: {
|
||||
fromAgent: string;
|
||||
recipientType: string;
|
||||
subject: string;
|
||||
body: string;
|
||||
}): AgentMessage[] {
|
||||
// In a real system, you'd look up agents by type.
|
||||
// For now, we log the broadcast and return a single message.
|
||||
const msg = this.send({
|
||||
fromAgent: params.fromAgent,
|
||||
toAgent: `broadcast:${params.recipientType}`,
|
||||
type: "direct",
|
||||
subject: params.subject,
|
||||
body: params.body,
|
||||
});
|
||||
return [msg];
|
||||
}
|
||||
|
||||
/** Delegate a subtask to a specific agent */
|
||||
delegate(params: {
|
||||
fromAgent: string;
|
||||
toAgent: string;
|
||||
task: string;
|
||||
context?: string[];
|
||||
}): AgentMessage {
|
||||
return this.send({
|
||||
fromAgent: params.fromAgent,
|
||||
toAgent: params.toAgent,
|
||||
type: "delegation",
|
||||
subject: `Subtask: ${params.task.slice(0, 80)}`,
|
||||
body: params.task,
|
||||
priority: "high",
|
||||
});
|
||||
}
|
||||
|
||||
/** Send a contradiction resolution request */
|
||||
raiseContradiction(params: {
|
||||
fromAgent: string;
|
||||
toAgent: string;
|
||||
claim: string;
|
||||
counterClaim: string;
|
||||
evidence: string[];
|
||||
}): AgentMessage {
|
||||
return this.send({
|
||||
fromAgent: params.fromAgent,
|
||||
toAgent: params.toAgent,
|
||||
type: "contradiction",
|
||||
subject: `Contradiction: ${params.claim.slice(0, 60)} vs ${params.counterClaim.slice(0, 60)}`,
|
||||
body: `Claim: ${params.claim}\nCounter: ${params.counterClaim}\nEvidence:\n${params.evidence.join("\n")}`,
|
||||
priority: "high",
|
||||
});
|
||||
}
|
||||
|
||||
/** Get inbox for an agent */
|
||||
getInbox(agentId: string): AgentMessage[] {
|
||||
return this.messages
|
||||
.filter((m) => m.toAgent === agentId || m.toAgent.startsWith("broadcast:"))
|
||||
.sort((a, b) => {
|
||||
const priorityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
|
||||
const pDiff = priorityOrder[a.priority] - priorityOrder[b.priority];
|
||||
if (pDiff !== 0) return pDiff;
|
||||
return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime();
|
||||
});
|
||||
}
|
||||
|
||||
/** Get messages from a specific thread */
|
||||
getThread(parentMessageId: string): AgentMessage[] {
|
||||
return this.messages
|
||||
.filter((m) => m.id === parentMessageId || m.parentMessageId === parentMessageId)
|
||||
.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime());
|
||||
}
|
||||
|
||||
/** Mark a message as delivered */
|
||||
markDelivered(messageId: string): void {
|
||||
const msg = this.messages.find((m) => m.id === messageId);
|
||||
if (msg) {
|
||||
msg.status = "delivered";
|
||||
msg.deliveredAt = new Date().toISOString();
|
||||
this.persist();
|
||||
}
|
||||
}
|
||||
|
||||
/** Mark a message as resolved */
|
||||
markResolved(messageId: string): void {
|
||||
const msg = this.messages.find((m) => m.id === messageId);
|
||||
if (msg) {
|
||||
msg.status = "resolved";
|
||||
msg.resolvedAt = new Date().toISOString();
|
||||
this.persist();
|
||||
}
|
||||
}
|
||||
|
||||
/** Subscribe to messages for an agent */
|
||||
subscribe(agentId: string, callback: (msg: AgentMessage) => void): () => void {
|
||||
const existing = this.subscriptions.get(agentId) ?? [];
|
||||
existing.push(callback);
|
||||
this.subscriptions.set(agentId, existing);
|
||||
|
||||
// Return unsubscribe function
|
||||
return () => {
|
||||
const subs = this.subscriptions.get(agentId) ?? [];
|
||||
const idx = subs.indexOf(callback);
|
||||
if (idx >= 0) subs.splice(idx, 1);
|
||||
};
|
||||
}
|
||||
|
||||
/** Get all active contradictions */
|
||||
getActiveContradictions(): AgentMessage[] {
|
||||
return this.messages.filter(
|
||||
(m) => m.type === "contradiction" && m.status !== "resolved"
|
||||
);
|
||||
}
|
||||
|
||||
/** Synthesize results from multiple agents */
|
||||
synthesize(agentIds: string[], task: string): string {
|
||||
const relevantMessages = this.messages.filter(
|
||||
(m) => m.type === "result" && agentIds.includes(m.fromAgent)
|
||||
);
|
||||
|
||||
if (relevantMessages.length === 0) {
|
||||
return `No results from requested agents for task: ${task}`;
|
||||
}
|
||||
|
||||
const lines = [`## Synthesis: ${task}`, ""];
|
||||
for (const msg of relevantMessages) {
|
||||
lines.push(`### From ${msg.fromAgent}`);
|
||||
lines.push(msg.body.slice(0, 300));
|
||||
lines.push("");
|
||||
}
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
private persist(): void {
|
||||
this.messages = this.messages.slice(-500); // Keep last 500 messages
|
||||
this.store.write("messages", "index.json", this.messages);
|
||||
}
|
||||
|
||||
private load(): void {
|
||||
const data = this.store.read<AgentMessage[]>("messages", "index.json");
|
||||
if (data) this.messages = data;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,202 @@
|
|||
/**
|
||||
* Agent Observability — emergent behavior detection for multi-agent collectives.
|
||||
*
|
||||
* From arXiv:2606.12683 (Genewein et al.): "Frictions in multi-agent collectives
|
||||
* include emergent behaviors that are hard to predict or detect."
|
||||
*
|
||||
* This module tracks agent interactions, detects patterns that indicate emergent
|
||||
* behavior, and alerts when agents coordinate in unexpected ways.
|
||||
*
|
||||
* Detection signals:
|
||||
* 1. Agent interaction graph — who talks to whom, how often
|
||||
* 2. Behavioral drift — an agent's outputs change significantly over time
|
||||
* 3. Collusion patterns — agents repeatedly agree when they should verify
|
||||
* 4. Echo chamber — agents reinforce each other's errors without correction
|
||||
* 5. Task drift — agents gradually expand scope beyond their assignment
|
||||
*/
|
||||
|
||||
import { StateStore } from "../core/state-store.js";
|
||||
|
||||
export interface AgentEvent {
|
||||
timestamp: string;
|
||||
agentId: string;
|
||||
agentRole: string;
|
||||
eventType: "spawned" | "completed" | "error" | "communication" | "verdict" | "drift";
|
||||
details: string;
|
||||
relatedAgentIds: string[];
|
||||
}
|
||||
|
||||
export interface AgentInteraction {
|
||||
fromAgent: string;
|
||||
toAgent: string;
|
||||
type: "delegates_to" | "verifies" | "contradicts" | "agrees_with" | "references";
|
||||
count: number;
|
||||
lastSeen: string;
|
||||
}
|
||||
|
||||
export interface EmergentSignal {
|
||||
type: "collusion" | "echo_chamber" | "task_drift" | "behavioral_drift" | "interaction_spike";
|
||||
severity: "low" | "medium" | "high" | "critical";
|
||||
description: string;
|
||||
agents: string[];
|
||||
evidence: string[];
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
export class AgentObservability {
|
||||
private store: StateStore;
|
||||
private events: AgentEvent[] = [];
|
||||
private interactions: Map<string, AgentInteraction> = new Map();
|
||||
|
||||
constructor(store?: StateStore) {
|
||||
this.store = store ?? new StateStore();
|
||||
this.store.ensureSubDir("observability");
|
||||
this.load();
|
||||
}
|
||||
|
||||
/** Record an agent event */
|
||||
record(event: AgentEvent): void {
|
||||
this.events.push(event);
|
||||
this.store.append("observability", "events.jsonl", event);
|
||||
|
||||
// Update interaction graph
|
||||
for (const relatedId of event.relatedAgentIds) {
|
||||
const key = `${event.agentId}|${relatedId}`;
|
||||
const existing = this.interactions.get(key);
|
||||
if (existing) {
|
||||
existing.count++;
|
||||
existing.lastSeen = event.timestamp;
|
||||
if (event.eventType === "verdict") {
|
||||
existing.type = event.details.includes("pass") ? "agrees_with" : "contradicts";
|
||||
}
|
||||
} else {
|
||||
this.interactions.set(key, {
|
||||
fromAgent: event.agentId,
|
||||
toAgent: relatedId,
|
||||
type: "references",
|
||||
count: 1,
|
||||
lastSeen: event.timestamp,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Scan for emergent behavior signals */
|
||||
scan(): EmergentSignal[] {
|
||||
const signals: EmergentSignal[] = [];
|
||||
const recentEvents = this.events.slice(-100);
|
||||
|
||||
// 1. Collusion: repeated agreement without verification
|
||||
const agreementChain = recentEvents.filter(
|
||||
(e) => e.eventType === "verdict" && e.details.includes("pass")
|
||||
);
|
||||
if (agreementChain.length > 5) {
|
||||
const uniqueAgents = new Set(agreementChain.map((e) => e.agentId));
|
||||
if (uniqueAgents.size < agreementChain.length * 0.5) {
|
||||
signals.push({
|
||||
type: "collusion",
|
||||
severity: "high",
|
||||
description: `${agreementChain.length} consecutive passes from ${uniqueAgents.size} agents — possible collusion`,
|
||||
agents: [...uniqueAgents],
|
||||
evidence: agreementChain.slice(-3).map((e) => `${e.agentId}: ${e.details}`),
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Echo chamber: agents repeatedly reference each other without contradiction
|
||||
const contradictCount = recentEvents.filter(
|
||||
(e) => e.eventType === "verdict" && e.details.includes("fail")
|
||||
).length;
|
||||
const totalVerdicts = recentEvents.filter((e) => e.eventType === "verdict").length;
|
||||
if (totalVerdicts > 5 && contradictCount === 0) {
|
||||
signals.push({
|
||||
type: "echo_chamber",
|
||||
severity: "medium",
|
||||
description: `Zero contradictions in ${totalVerdicts} verdicts — possible echo chamber`,
|
||||
agents: [...new Set(recentEvents.map((e) => e.agentId))],
|
||||
evidence: [`${contradictCount}/${totalVerdicts} verdicts were failures`],
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
|
||||
// 3. Behavioral drift: agents changing their output patterns
|
||||
const agentGroups = new Map<string, AgentEvent[]>();
|
||||
for (const e of recentEvents) {
|
||||
const existing = agentGroups.get(e.agentId) ?? [];
|
||||
existing.push(e);
|
||||
agentGroups.set(e.agentId, existing);
|
||||
}
|
||||
|
||||
for (const [agentId, agentEvents] of agentGroups) {
|
||||
if (agentEvents.length < 5) continue;
|
||||
|
||||
const recent = agentEvents.slice(-3);
|
||||
const old = agentEvents.slice(0, 3);
|
||||
const recentAvgLength = recent.reduce((s, e) => s + e.details.length, 0) / recent.length;
|
||||
const oldAvgLength = old.reduce((s, e) => s + e.details.length, 0) / old.length;
|
||||
|
||||
if (recentAvgLength > oldAvgLength * 2 || recentAvgLength < oldAvgLength * 0.3) {
|
||||
signals.push({
|
||||
type: "behavioral_drift",
|
||||
severity: "medium",
|
||||
description: `Agent "${agentId}" output length changed from ${Math.round(oldAvgLength)} to ${Math.round(recentAvgLength)} chars`,
|
||||
agents: [agentId],
|
||||
evidence: [`Old avg: ${Math.round(oldAvgLength)}`, `Recent avg: ${Math.round(recentAvgLength)}`],
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Interaction spike: unusually high communication between specific agents
|
||||
const highInteractionKeys = [...this.interactions.values()]
|
||||
.filter((i) => i.count > 5 && i.type !== "references")
|
||||
.sort((a, b) => b.count - a.count)
|
||||
.slice(0, 3);
|
||||
|
||||
for (const interaction of highInteractionKeys) {
|
||||
signals.push({
|
||||
type: "interaction_spike",
|
||||
severity: "low",
|
||||
description: `High interaction: ${interaction.fromAgent} → ${interaction.toAgent} (${interaction.count}x, ${interaction.type})`,
|
||||
agents: [interaction.fromAgent, interaction.toAgent],
|
||||
evidence: [`${interaction.count} interactions`, `Type: ${interaction.type}`],
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
|
||||
// Persist signals
|
||||
for (const signal of signals) {
|
||||
this.store.append("observability", "signals.jsonl", signal);
|
||||
}
|
||||
|
||||
return signals;
|
||||
}
|
||||
|
||||
/** Get interaction graph */
|
||||
getInteractionGraph(): AgentInteraction[] {
|
||||
return [...this.interactions.values()].sort((a, b) => b.count - a.count);
|
||||
}
|
||||
|
||||
/** Get agent summary */
|
||||
getAgentSummary(agentId: string): { events: number; lastSeen: string; roles: string[] } {
|
||||
const agentEvents = this.events.filter((e) => e.agentId === agentId);
|
||||
const roles = [...new Set(agentEvents.map((e) => e.agentRole))];
|
||||
return {
|
||||
events: agentEvents.length,
|
||||
lastSeen: agentEvents.length > 0 ? agentEvents[agentEvents.length - 1].timestamp : "never",
|
||||
roles,
|
||||
};
|
||||
}
|
||||
|
||||
/** Get recent signals */
|
||||
getRecentSignals(n: number = 10): EmergentSignal[] {
|
||||
return this.store.readLines<EmergentSignal>("observability", "signals.jsonl")
|
||||
.slice(-n)
|
||||
.reverse();
|
||||
}
|
||||
|
||||
private load(): void {
|
||||
this.events = this.store.readLines<AgentEvent>("observability", "events.jsonl");
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,176 @@
|
|||
/**
|
||||
* Agent Specialization — auto-route sub-agents to tasks based on historical performance.
|
||||
*
|
||||
* From arXiv:2606.12683 (Genewein et al.): "Optimal collective performance requires
|
||||
* agents to specialize — different agents for different task types."
|
||||
*
|
||||
* Instead of routing all sub-agents identically, this module tracks each agent's
|
||||
* performance by task type and routes future tasks to the best-performing agents.
|
||||
*
|
||||
* This is the multi-agent version of AdaptiveRouter.
|
||||
*/
|
||||
|
||||
import { StateStore } from "../core/state-store.js";
|
||||
|
||||
export type TaskType = "research" | "code" | "analysis" | "review" | "generation" | "verification";
|
||||
|
||||
export interface AgentPerformance {
|
||||
agentId: string;
|
||||
executorType: string;
|
||||
taskType: TaskType;
|
||||
totalTasks: number;
|
||||
successRate: number;
|
||||
avgQuality: number;
|
||||
avgDurationMs: number;
|
||||
lastTaskAt: string;
|
||||
score: number;
|
||||
}
|
||||
|
||||
export interface RoutingDecision {
|
||||
agentId: string;
|
||||
executorType: string;
|
||||
taskType: TaskType;
|
||||
confidence: number;
|
||||
reason: string;
|
||||
}
|
||||
|
||||
export class AgentSpecialization {
|
||||
private store: StateStore;
|
||||
private performance: Map<string, AgentPerformance[]> = new Map();
|
||||
|
||||
constructor(store?: StateStore) {
|
||||
this.store = store ?? new StateStore();
|
||||
this.store.ensureSubDir("specialization");
|
||||
this.load();
|
||||
}
|
||||
|
||||
/** Record an agent's performance on a task */
|
||||
record(params: {
|
||||
agentId: string;
|
||||
executorType: string;
|
||||
taskType: TaskType;
|
||||
success: boolean;
|
||||
quality: number;
|
||||
durationMs: number;
|
||||
}): void {
|
||||
const key = `${params.executorType}|${params.taskType}`;
|
||||
const existing = this.performance.get(key) ?? [];
|
||||
const record = existing.find((p) => p.agentId === params.agentId);
|
||||
|
||||
if (record) {
|
||||
const total = record.totalTasks + 1;
|
||||
record.totalTasks = total;
|
||||
record.successRate = (record.successRate * (total - 1) + (params.success ? 1 : 0)) / total;
|
||||
record.avgQuality = (record.avgQuality * (total - 1) + params.quality) / total;
|
||||
record.avgDurationMs = (record.avgDurationMs * (total - 1) + params.durationMs) / total;
|
||||
record.lastTaskAt = new Date().toISOString();
|
||||
record.score = this.calculateScore(record);
|
||||
} else {
|
||||
const perf: AgentPerformance = {
|
||||
agentId: params.agentId,
|
||||
executorType: params.executorType,
|
||||
taskType: params.taskType,
|
||||
totalTasks: 1,
|
||||
successRate: params.success ? 1 : 0,
|
||||
avgQuality: params.quality,
|
||||
avgDurationMs: params.durationMs,
|
||||
lastTaskAt: new Date().toISOString(),
|
||||
score: params.quality * (params.success ? 1 : 0.3),
|
||||
};
|
||||
existing.push(perf);
|
||||
}
|
||||
|
||||
this.performance.set(key, existing);
|
||||
this.persist();
|
||||
}
|
||||
|
||||
/** Route a task to the best-performing agent for that task type */
|
||||
route(taskType: TaskType): RoutingDecision {
|
||||
const allRecords = [...this.performance.values()].flat();
|
||||
const forType = allRecords
|
||||
.filter((p) => p.taskType === taskType)
|
||||
.sort((a, b) => b.score - a.score);
|
||||
|
||||
if (forType.length === 0) {
|
||||
// No history — return a generic routing decision
|
||||
return {
|
||||
agentId: "new-agent",
|
||||
executorType: this.defaultExecutor(taskType),
|
||||
taskType,
|
||||
confidence: 0.3,
|
||||
reason: "No historical data for this task type — using default executor",
|
||||
};
|
||||
}
|
||||
|
||||
const best = forType[0];
|
||||
return {
|
||||
agentId: best.agentId,
|
||||
executorType: best.executorType,
|
||||
taskType,
|
||||
confidence: Math.min(best.score, 0.95),
|
||||
reason: `Best performer for ${taskType}: ${(best.successRate * 100).toFixed(0)}% success, ${best.avgQuality.toFixed(2)} quality (${best.totalTasks} tasks)`,
|
||||
};
|
||||
}
|
||||
|
||||
/** Get specialization report: which agent is best for each task type */
|
||||
getSpecializationReport(): string {
|
||||
const allRecords = [...this.performance.values()].flat();
|
||||
const taskTypes = [...new Set(allRecords.map((r) => r.taskType))];
|
||||
|
||||
const lines = ["Agent Specialization Report", ""];
|
||||
for (const taskType of taskTypes) {
|
||||
const sorted = allRecords
|
||||
.filter((r) => r.taskType === taskType)
|
||||
.sort((a, b) => b.score - a.score);
|
||||
|
||||
if (sorted.length === 0) continue;
|
||||
|
||||
const best = sorted[0];
|
||||
lines.push(` ${taskType}:`);
|
||||
lines.push(` Best: ${best.agentId} (${best.executorType}) — ${(best.successRate * 100).toFixed(0)}% success, ${best.totalTasks} tasks`);
|
||||
if (sorted.length > 1) {
|
||||
lines.push(` Runner-up: ${sorted[1].agentId} (${sorted[1].executorType}) — ${(sorted[1].successRate * 100).toFixed(0)}% success`);
|
||||
}
|
||||
lines.push("");
|
||||
}
|
||||
|
||||
if (lines.length <= 2) {
|
||||
lines.push(" No performance data yet — route tasks to build history.");
|
||||
}
|
||||
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
private calculateScore(perf: AgentPerformance): number {
|
||||
return (perf.successRate * 0.4) + (perf.avgQuality * 0.4) + (Math.min(perf.totalTasks / 10, 1) * 0.2);
|
||||
}
|
||||
|
||||
private defaultExecutor(taskType: TaskType): string {
|
||||
const map: Record<TaskType, string> = {
|
||||
research: "exa-search",
|
||||
code: "sonnet-4-6",
|
||||
analysis: "opus-4-8",
|
||||
review: "haiku",
|
||||
generation: "gpt-4.1",
|
||||
verification: "haiku",
|
||||
};
|
||||
return map[taskType];
|
||||
}
|
||||
|
||||
private persist(): void {
|
||||
const data: Record<string, AgentPerformance[]> = {};
|
||||
for (const [key, records] of this.performance) {
|
||||
data[key] = records;
|
||||
}
|
||||
this.store.write("specialization", "performance.json", data);
|
||||
}
|
||||
|
||||
private load(): void {
|
||||
const data = this.store.read<Record<string, AgentPerformance[]>>("specialization", "performance.json");
|
||||
if (data) {
|
||||
for (const [key, records] of Object.entries(data)) {
|
||||
this.performance.set(key, records);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue