fill ASI gaps: agent observability, specialization optimizer, message bus

This commit is contained in:
artale 2026-06-13 13:17:20 +02:00
parent 4d9628db8b
commit 3fd9758da3
4 changed files with 675 additions and 0 deletions

View File

@ -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();

View File

@ -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;
}
}

View File

@ -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");
}
}

View File

@ -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);
}
}
}
}