Kraft102's picture
Initial deployment - WidgeTDC Cortex Backend v2.1.0
529090e
/**
* ╔═══════════════════════════════════════════════════════════════════════════╗
* β•‘ CAPABILITY BROKER β•‘
* ║═══════════════════════════════════════════════════════════════════════════║
* β•‘ Routes capability requests mellem agenter β•‘
* β•‘ Bruger BΓ…DE Blackboard (async) og Neural Chat (sync) β•‘
* β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•
*/
import { v4 as uuidv4 } from 'uuid';
import * as fs from 'fs/promises';
import * as path from 'path';
import { neo4jAdapter } from '../../adapters/Neo4jAdapter.js';
import { neuralChatService } from './ChatService.js';
import { approvalGate } from './ApprovalGate.js';
import { AgentId } from './types.js';
import {
CapabilityRequest,
CapabilityResponse,
AGENT_CAPABILITIES,
AgentCapability
} from './AgentCapabilities.js';
import { DROPZONE_PATH } from '../../config.js';
class CapabilityBroker {
private static instance: CapabilityBroker;
private pendingRequests: Map<string, CapabilityRequest> = new Map();
private constructor() {}
public static getInstance(): CapabilityBroker {
if (!CapabilityBroker.instance) {
CapabilityBroker.instance = new CapabilityBroker();
}
return CapabilityBroker.instance;
}
/**
* Find den bedste agent til en given capability
*/
findBestAgent(capabilityType: string): AgentCapability | null {
for (const [agentId, capabilities] of Object.entries(AGENT_CAPABILITIES)) {
const match = capabilities.find(c =>
c.id === capabilityType ||
c.name.toLowerCase().includes(capabilityType.toLowerCase()) ||
c.category === capabilityType
);
if (match) return match;
}
return null;
}
/**
* List alle capabilities for en agent
*/
getAgentCapabilities(agent: AgentId): AgentCapability[] {
return AGENT_CAPABILITIES[agent] || [];
}
/**
* List ALLE capabilities pΓ₯ tvΓ¦rs af agenter
*/
getAllCapabilities(): AgentCapability[] {
return Object.values(AGENT_CAPABILITIES).flat();
}
/**
* Request en capability fra en anden agent
* CLAUDE REVIEWS ALL REQUESTS (delegeret fra CLAK)
*/
async requestCapability(params: {
fromAgent: AgentId;
toAgent: AgentId;
capability: string;
params: Record<string, any>;
priority?: 'low' | 'normal' | 'high' | 'critical';
deadline?: string;
}): Promise<CapabilityRequest & { approval?: any }> {
// ════════════════════════════════════════════════════════════
// STEP 1: Claude reviews the request (delegated from CLAK)
// ════════════════════════════════════════════════════════════
const approval = approvalGate.review({
fromAgent: params.fromAgent,
action: params.capability,
description: `${params.fromAgent} wants to use ${params.capability} capability`,
context: params.params
});
console.log(`[ApprovalGate] ${params.fromAgent} -> ${params.capability}: ${approval.decision}`);
// If escalation needed, notify and pause
if (approval.decision === 'escalate_to_clak') {
await neuralChatService.sendMessage({
channel: 'alerts',
from: 'system',
body: `[ESCALATION REQUIRED]\n\nAgent: ${params.fromAgent}\nAction: ${params.capability}\nReason: ${approval.reason}\n\n@clak please review and decide.`,
type: 'alert',
priority: 'critical',
mentions: ['clak']
});
throw new Error(`Action requires CLAK approval: ${approval.reason}`);
}
// If rejected, notify and stop
if (approval.decision === 'rejected') {
await neuralChatService.sendMessage({
channel: 'core-dev',
from: 'claude',
body: `[DENIED] Request Denied\n\nFrom: ${params.fromAgent}\nAction: ${params.capability}\nReason: ${approval.reason}`,
type: 'status',
priority: 'normal'
});
throw new Error(`Request denied by Claude: ${approval.reason}`);
}
// ════════════════════════════════════════════════════════════
// STEP 2: Approved - proceed with request
// ════════════════════════════════════════════════════════════
const request: CapabilityRequest = {
requestId: `cap-${uuidv4()}`,
fromAgent: params.fromAgent,
toAgent: params.toAgent,
capability: params.capability,
params: params.params,
priority: params.priority || 'normal',
timestamp: new Date().toISOString(),
deadline: params.deadline
};
// 1. Gem i memory
this.pendingRequests.set(request.requestId, request);
// 2. Gem i Blackboard (fil-baseret) for async pickup
await this.saveToBlackboard(request);
// 3. Gem i Neo4j for persistence og queries
await this.persistToNeo4j(request);
// 4. Send notifikation via Neural Chat (with approval note)
const modNote = approval.modifications?.length
? `\n[Notes]: ${approval.modifications.join(', ')}`
: '';
await neuralChatService.sendMessage({
channel: 'core-dev',
from: 'claude',
body: `[APPROVED] Capability Request\n\nCapability: ${params.capability}\nFrom: ${params.fromAgent} -> To: ${params.toAgent}\nPriority: ${params.priority || 'normal'}\nRequest ID: ${request.requestId}${modNote}`,
type: 'task',
priority: params.priority || 'normal',
mentions: [params.toAgent]
});
console.log(`[CapabilityBroker] APPROVED: ${request.requestId}: ${params.fromAgent} -> ${params.toAgent} (${params.capability})`);
return { ...request, approval };
}
/**
* Gem request i Blackboard (fil-system)
*/
private async saveToBlackboard(request: CapabilityRequest): Promise<void> {
const filename = `${request.timestamp.replace(/[:.]/g, '-')}_capability_${request.requestId}.json`;
const inboxPath = path.join(DROPZONE_PATH, 'agents', request.toAgent, 'inbox', filename);
try {
await fs.mkdir(path.dirname(inboxPath), { recursive: true });
await fs.writeFile(inboxPath, JSON.stringify({
...request,
_type: 'capability_request',
_blackboard_version: '2.0'
}, null, 2));
} catch (error) {
console.warn('Failed to save to Blackboard:', error);
}
}
/**
* Persist request til Neo4j
*/
private async persistToNeo4j(request: CapabilityRequest): Promise<void> {
try {
await neo4jAdapter.executeQuery(`
CREATE (r:CapabilityRequest {
requestId: $requestId,
fromAgent: $fromAgent,
toAgent: $toAgent,
capability: $capability,
params: $params,
priority: $priority,
timestamp: $timestamp,
deadline: $deadline,
status: 'pending'
})
WITH r
MATCH (from:Agent {name: $fromAgent})
MATCH (to:Agent {name: $toAgent})
MERGE (from)-[:REQUESTED]->(r)
MERGE (r)-[:ASSIGNED_TO]->(to)
`, {
requestId: request.requestId,
fromAgent: request.fromAgent,
toAgent: request.toAgent,
capability: request.capability,
params: JSON.stringify(request.params),
priority: request.priority,
timestamp: request.timestamp,
deadline: request.deadline || ''
});
} catch (error) {
console.warn('Failed to persist to Neo4j:', error);
}
}
/**
* Besvar en capability request
*/
async respondToCapability(params: {
requestId: string;
success: boolean;
result?: any;
error?: string;
respondingAgent: AgentId;
}): Promise<CapabilityResponse> {
const response: CapabilityResponse = {
requestId: params.requestId,
success: params.success,
result: params.result,
error: params.error,
timestamp: new Date().toISOString()
};
// Fjern fra pending
const request = this.pendingRequests.get(params.requestId);
this.pendingRequests.delete(params.requestId);
// Opdater Neo4j
try {
await neo4jAdapter.executeQuery(`
MATCH (r:CapabilityRequest {requestId: $requestId})
SET r.status = $status,
r.response = $response,
r.completedAt = $completedAt
`, {
requestId: params.requestId,
status: params.success ? 'completed' : 'failed',
response: JSON.stringify(response),
completedAt: response.timestamp
});
} catch (error) {
console.warn('Failed to update Neo4j:', error);
}
// Notificer via Neural Chat
if (request) {
await neuralChatService.sendMessage({
channel: 'core-dev',
from: params.respondingAgent,
body: `[RESPONSE] Capability Response: ${params.requestId}\nStatus: ${params.success ? 'SUCCESS' : 'FAILED'}\n${params.error || ''}`,
type: 'response',
priority: 'normal',
mentions: [request.fromAgent]
});
}
return response;
}
/**
* Hent pending requests for en agent
*/
async getPendingRequests(agent: AgentId): Promise<CapabilityRequest[]> {
try {
const results = await neo4jAdapter.executeQuery(`
MATCH (r:CapabilityRequest {toAgent: $agent, status: 'pending'})
RETURN r
ORDER BY r.timestamp DESC
`, { agent });
return results.map((row: any) => ({
requestId: row.r.properties.requestId,
fromAgent: row.r.properties.fromAgent,
toAgent: row.r.properties.toAgent,
capability: row.r.properties.capability,
params: JSON.parse(row.r.properties.params || '{}'),
priority: row.r.properties.priority,
timestamp: row.r.properties.timestamp,
deadline: row.r.properties.deadline
}));
} catch (error) {
console.warn('Failed to fetch pending requests:', error);
return Array.from(this.pendingRequests.values())
.filter(r => r.toAgent === agent);
}
}
/**
* Smart routing: Find bedste agent til en opgave baseret pΓ₯ capability match
*/
async smartRoute(params: {
task: string;
context?: string;
fromAgent: AgentId;
}): Promise<{ agent: AgentId; capability: AgentCapability; confidence: number } | null> {
const allCapabilities = this.getAllCapabilities();
// Simple keyword matching (kan udvides med embeddings)
const taskLower = params.task.toLowerCase();
let bestMatch: { agent: AgentId; capability: AgentCapability; confidence: number } | null = null;
for (const cap of allCapabilities) {
let score = 0;
// Check name match
if (taskLower.includes(cap.name.toLowerCase())) score += 0.5;
// Check description match
const descWords = cap.description.toLowerCase().split(' ');
const matchingWords = descWords.filter(w => taskLower.includes(w) && w.length > 3);
score += matchingWords.length * 0.1;
// Check category match
if (taskLower.includes(cap.category)) score += 0.3;
// Factor in reliability
score *= cap.reliability || 0.8;
if (!bestMatch || score > bestMatch.confidence) {
bestMatch = {
agent: cap.agent,
capability: cap,
confidence: Math.min(score, 1.0)
};
}
}
return bestMatch;
}
}
export const capabilityBroker = CapabilityBroker.getInstance();
// Re-export types (use 'export type' for interfaces)
export { AGENT_CAPABILITIES } from './AgentCapabilities.js';
export type { AgentCapability, CapabilityRequest, CapabilityResponse } from './AgentCapabilities.js';