diff --git a/src/examples/test-asi-gaps.ts b/src/examples/test-asi-gaps.ts new file mode 100644 index 0000000..f88bf1c --- /dev/null +++ b/src/examples/test-asi-gaps.ts @@ -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(); diff --git a/src/upgrades/agent-message-bus.ts b/src/upgrades/agent-message-bus.ts new file mode 100644 index 0000000..bd9d021 --- /dev/null +++ b/src/upgrades/agent-message-bus.ts @@ -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 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("messages", "index.json"); + if (data) this.messages = data; + } +} diff --git a/src/upgrades/agent-observability.ts b/src/upgrades/agent-observability.ts new file mode 100644 index 0000000..2efff4d --- /dev/null +++ b/src/upgrades/agent-observability.ts @@ -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 = 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(); + 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("observability", "signals.jsonl") + .slice(-n) + .reverse(); + } + + private load(): void { + this.events = this.store.readLines("observability", "events.jsonl"); + } +} diff --git a/src/upgrades/agent-specialization.ts b/src/upgrades/agent-specialization.ts new file mode 100644 index 0000000..79d190c --- /dev/null +++ b/src/upgrades/agent-specialization.ts @@ -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 = 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 = { + 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 = {}; + 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>("specialization", "performance.json"); + if (data) { + for (const [key, records] of Object.entries(data)) { + this.performance.set(key, records); + } + } + } +}