agentic-flow 1.8.10 → 1.8.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agents/claudeAgent.js +50 -0
- package/dist/cli/federation-cli.d.ts +53 -0
- package/dist/cli/federation-cli.js +431 -0
- package/dist/cli-proxy.js +28 -1
- package/dist/federation/EphemeralAgent.js +258 -0
- package/dist/federation/FederationHub.js +283 -0
- package/dist/federation/FederationHubClient.js +212 -0
- package/dist/federation/FederationHubServer.js +436 -0
- package/dist/federation/SecurityManager.js +191 -0
- package/dist/federation/debug/agent-debug-stream.js +474 -0
- package/dist/federation/debug/debug-stream.js +419 -0
- package/dist/federation/index.js +12 -0
- package/dist/federation/integrations/realtime-federation.js +404 -0
- package/dist/federation/integrations/supabase-adapter-debug.js +400 -0
- package/dist/federation/integrations/supabase-adapter.js +258 -0
- package/dist/index.js +18 -1
- package/dist/utils/cli.js +5 -0
- package/docs/architecture/FEDERATION-DATA-LIFECYCLE.md +520 -0
- package/docs/federation/AGENT-DEBUG-STREAMING.md +403 -0
- package/docs/federation/DEBUG-STREAMING-COMPLETE.md +432 -0
- package/docs/federation/DEBUG-STREAMING.md +537 -0
- package/docs/federation/DEPLOYMENT-VALIDATION-SUCCESS.md +394 -0
- package/docs/federation/DOCKER-FEDERATION-DEEP-REVIEW.md +478 -0
- package/docs/issues/ISSUE-SUPABASE-INTEGRATION.md +536 -0
- package/docs/supabase/IMPLEMENTATION-SUMMARY.md +498 -0
- package/docs/supabase/INDEX.md +358 -0
- package/docs/supabase/QUICKSTART.md +365 -0
- package/docs/supabase/README.md +318 -0
- package/docs/supabase/SUPABASE-REALTIME-FEDERATION.md +575 -0
- package/docs/supabase/TEST-REPORT.md +446 -0
- package/docs/supabase/migrations/001_create_federation_tables.sql +339 -0
- package/docs/validation/reports/REGRESSION-TEST-V1.8.11.md +456 -0
- package/package.json +4 -1
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Federation Hub Client - WebSocket client for agent-to-hub communication
|
|
3
|
+
*/
|
|
4
|
+
import WebSocket from 'ws';
|
|
5
|
+
import { logger } from '../utils/logger.js';
|
|
6
|
+
export class FederationHubClient {
|
|
7
|
+
config;
|
|
8
|
+
ws;
|
|
9
|
+
connected = false;
|
|
10
|
+
vectorClock = {};
|
|
11
|
+
lastSyncTime = 0;
|
|
12
|
+
messageHandlers = new Map();
|
|
13
|
+
constructor(config) {
|
|
14
|
+
this.config = config;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Connect to hub with WebSocket
|
|
18
|
+
*/
|
|
19
|
+
async connect() {
|
|
20
|
+
return new Promise((resolve, reject) => {
|
|
21
|
+
try {
|
|
22
|
+
// Convert quic:// to ws:// for WebSocket connection
|
|
23
|
+
const wsEndpoint = this.config.endpoint
|
|
24
|
+
.replace('quic://', 'ws://')
|
|
25
|
+
.replace(':4433', ':8443'); // Map QUIC port to WebSocket port
|
|
26
|
+
logger.info('Connecting to federation hub', {
|
|
27
|
+
endpoint: wsEndpoint,
|
|
28
|
+
agentId: this.config.agentId
|
|
29
|
+
});
|
|
30
|
+
this.ws = new WebSocket(wsEndpoint);
|
|
31
|
+
this.ws.on('open', async () => {
|
|
32
|
+
logger.info('WebSocket connected, authenticating...');
|
|
33
|
+
// Send authentication
|
|
34
|
+
await this.send({
|
|
35
|
+
type: 'auth',
|
|
36
|
+
agentId: this.config.agentId,
|
|
37
|
+
tenantId: this.config.tenantId,
|
|
38
|
+
token: this.config.token,
|
|
39
|
+
vectorClock: this.vectorClock,
|
|
40
|
+
timestamp: Date.now()
|
|
41
|
+
});
|
|
42
|
+
// Wait for auth acknowledgment
|
|
43
|
+
const authTimeout = setTimeout(() => {
|
|
44
|
+
reject(new Error('Authentication timeout'));
|
|
45
|
+
}, 5000);
|
|
46
|
+
const authHandler = (msg) => {
|
|
47
|
+
if (msg.type === 'ack') {
|
|
48
|
+
clearTimeout(authTimeout);
|
|
49
|
+
this.connected = true;
|
|
50
|
+
this.lastSyncTime = Date.now();
|
|
51
|
+
logger.info('Authenticated with hub');
|
|
52
|
+
resolve();
|
|
53
|
+
}
|
|
54
|
+
else if (msg.type === 'error') {
|
|
55
|
+
clearTimeout(authTimeout);
|
|
56
|
+
reject(new Error(msg.error || 'Authentication failed'));
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
this.messageHandlers.set('auth', authHandler);
|
|
60
|
+
});
|
|
61
|
+
this.ws.on('message', (data) => {
|
|
62
|
+
try {
|
|
63
|
+
const message = JSON.parse(data.toString());
|
|
64
|
+
this.handleMessage(message);
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
logger.error('Failed to parse message', { error: error.message });
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
this.ws.on('close', () => {
|
|
71
|
+
this.connected = false;
|
|
72
|
+
logger.info('Disconnected from hub');
|
|
73
|
+
});
|
|
74
|
+
this.ws.on('error', (error) => {
|
|
75
|
+
logger.error('WebSocket error', { error: error.message });
|
|
76
|
+
reject(error);
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
logger.error('Failed to connect to hub', { error: error.message });
|
|
81
|
+
reject(error);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Handle incoming message
|
|
87
|
+
*/
|
|
88
|
+
handleMessage(message) {
|
|
89
|
+
// Check for specific handlers first
|
|
90
|
+
const handler = this.messageHandlers.get('auth');
|
|
91
|
+
if (handler) {
|
|
92
|
+
handler(message);
|
|
93
|
+
this.messageHandlers.delete('auth');
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
// Handle sync responses
|
|
97
|
+
if (message.type === 'ack' && message.data) {
|
|
98
|
+
logger.debug('Received sync data', { count: message.data.length });
|
|
99
|
+
}
|
|
100
|
+
else if (message.type === 'error') {
|
|
101
|
+
logger.error('Hub error', { error: message.error });
|
|
102
|
+
}
|
|
103
|
+
// Update vector clock if provided
|
|
104
|
+
if (message.vectorClock) {
|
|
105
|
+
this.updateVectorClock(message.vectorClock);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Sync with hub
|
|
110
|
+
*/
|
|
111
|
+
async sync(db) {
|
|
112
|
+
if (!this.connected) {
|
|
113
|
+
throw new Error('Not connected to hub');
|
|
114
|
+
}
|
|
115
|
+
const startTime = Date.now();
|
|
116
|
+
try {
|
|
117
|
+
// Increment vector clock
|
|
118
|
+
this.vectorClock[this.config.agentId] =
|
|
119
|
+
(this.vectorClock[this.config.agentId] || 0) + 1;
|
|
120
|
+
// PULL: Get updates from hub
|
|
121
|
+
await this.send({
|
|
122
|
+
type: 'pull',
|
|
123
|
+
agentId: this.config.agentId,
|
|
124
|
+
tenantId: this.config.tenantId,
|
|
125
|
+
vectorClock: this.vectorClock,
|
|
126
|
+
timestamp: Date.now()
|
|
127
|
+
});
|
|
128
|
+
// Wait for response (simplified for now)
|
|
129
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
130
|
+
// PUSH: Send local changes to hub
|
|
131
|
+
const localChanges = await this.getLocalChanges(db);
|
|
132
|
+
if (localChanges.length > 0) {
|
|
133
|
+
await this.send({
|
|
134
|
+
type: 'push',
|
|
135
|
+
agentId: this.config.agentId,
|
|
136
|
+
tenantId: this.config.tenantId,
|
|
137
|
+
vectorClock: this.vectorClock,
|
|
138
|
+
data: localChanges,
|
|
139
|
+
timestamp: Date.now()
|
|
140
|
+
});
|
|
141
|
+
logger.info('Sync completed', {
|
|
142
|
+
agentId: this.config.agentId,
|
|
143
|
+
pushCount: localChanges.length,
|
|
144
|
+
duration: Date.now() - startTime
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
this.lastSyncTime = Date.now();
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
logger.error('Sync failed', { error: error.message });
|
|
151
|
+
throw error;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Get local changes from database
|
|
156
|
+
*/
|
|
157
|
+
async getLocalChanges(db) {
|
|
158
|
+
// Query recent episodes from local database
|
|
159
|
+
// This is a simplified version - in production, track changes since last sync
|
|
160
|
+
try {
|
|
161
|
+
// Get recent patterns from AgentDB
|
|
162
|
+
// For now, return empty array as placeholder
|
|
163
|
+
return [];
|
|
164
|
+
}
|
|
165
|
+
catch (error) {
|
|
166
|
+
logger.error('Failed to get local changes', { error });
|
|
167
|
+
return [];
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Update vector clock
|
|
172
|
+
*/
|
|
173
|
+
updateVectorClock(remoteVectorClock) {
|
|
174
|
+
for (const [agentId, ts] of Object.entries(remoteVectorClock)) {
|
|
175
|
+
this.vectorClock[agentId] = Math.max(this.vectorClock[agentId] || 0, ts);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Send message to hub
|
|
180
|
+
*/
|
|
181
|
+
async send(message) {
|
|
182
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
183
|
+
throw new Error('WebSocket not connected');
|
|
184
|
+
}
|
|
185
|
+
this.ws.send(JSON.stringify(message));
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Disconnect from hub
|
|
189
|
+
*/
|
|
190
|
+
async disconnect() {
|
|
191
|
+
if (this.ws) {
|
|
192
|
+
this.ws.close();
|
|
193
|
+
this.ws = undefined;
|
|
194
|
+
}
|
|
195
|
+
this.connected = false;
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Check connection status
|
|
199
|
+
*/
|
|
200
|
+
isConnected() {
|
|
201
|
+
return this.connected;
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Get sync stats
|
|
205
|
+
*/
|
|
206
|
+
getSyncStats() {
|
|
207
|
+
return {
|
|
208
|
+
lastSyncTime: this.lastSyncTime,
|
|
209
|
+
vectorClock: { ...this.vectorClock }
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
}
|
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Federation Hub Server - WebSocket-based hub for agent synchronization
|
|
3
|
+
*
|
|
4
|
+
* This is a production-ready implementation using WebSocket (HTTP/2 upgrade)
|
|
5
|
+
* as a fallback until native QUIC is implemented.
|
|
6
|
+
*/
|
|
7
|
+
import { WebSocketServer, WebSocket } from 'ws';
|
|
8
|
+
import { createServer } from 'http';
|
|
9
|
+
import { logger } from '../utils/logger.js';
|
|
10
|
+
import Database from 'better-sqlite3';
|
|
11
|
+
export class FederationHubServer {
|
|
12
|
+
config;
|
|
13
|
+
wss;
|
|
14
|
+
server;
|
|
15
|
+
connections = new Map();
|
|
16
|
+
db;
|
|
17
|
+
agentDB;
|
|
18
|
+
globalVectorClock = {};
|
|
19
|
+
constructor(config) {
|
|
20
|
+
this.config = {
|
|
21
|
+
port: 8443,
|
|
22
|
+
dbPath: ':memory:',
|
|
23
|
+
maxAgents: 1000,
|
|
24
|
+
syncInterval: 5000,
|
|
25
|
+
...config
|
|
26
|
+
};
|
|
27
|
+
// Initialize hub database (SQLite for metadata)
|
|
28
|
+
this.db = new Database(this.config.dbPath);
|
|
29
|
+
this.initializeDatabase();
|
|
30
|
+
// AgentDB integration optional - using SQLite for now
|
|
31
|
+
this.agentDB = null;
|
|
32
|
+
logger.info('Federation hub initialized with SQLite');
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Initialize hub database schema
|
|
36
|
+
*/
|
|
37
|
+
initializeDatabase() {
|
|
38
|
+
// Memory store: tenant-isolated episodes
|
|
39
|
+
this.db.exec(`
|
|
40
|
+
CREATE TABLE IF NOT EXISTS episodes (
|
|
41
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
42
|
+
tenant_id TEXT NOT NULL,
|
|
43
|
+
agent_id TEXT NOT NULL,
|
|
44
|
+
session_id TEXT NOT NULL,
|
|
45
|
+
task TEXT NOT NULL,
|
|
46
|
+
input TEXT NOT NULL,
|
|
47
|
+
output TEXT NOT NULL,
|
|
48
|
+
reward REAL NOT NULL,
|
|
49
|
+
critique TEXT,
|
|
50
|
+
success INTEGER NOT NULL,
|
|
51
|
+
tokens_used INTEGER DEFAULT 0,
|
|
52
|
+
latency_ms INTEGER DEFAULT 0,
|
|
53
|
+
vector_clock TEXT NOT NULL,
|
|
54
|
+
created_at INTEGER NOT NULL,
|
|
55
|
+
UNIQUE(tenant_id, agent_id, session_id, task)
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
CREATE INDEX IF NOT EXISTS idx_episodes_tenant ON episodes(tenant_id);
|
|
59
|
+
CREATE INDEX IF NOT EXISTS idx_episodes_task ON episodes(tenant_id, task);
|
|
60
|
+
CREATE INDEX IF NOT EXISTS idx_episodes_created ON episodes(created_at);
|
|
61
|
+
|
|
62
|
+
-- Change log for sync
|
|
63
|
+
CREATE TABLE IF NOT EXISTS change_log (
|
|
64
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
65
|
+
tenant_id TEXT NOT NULL,
|
|
66
|
+
agent_id TEXT NOT NULL,
|
|
67
|
+
operation TEXT NOT NULL,
|
|
68
|
+
episode_id INTEGER,
|
|
69
|
+
vector_clock TEXT NOT NULL,
|
|
70
|
+
created_at INTEGER NOT NULL,
|
|
71
|
+
FOREIGN KEY(episode_id) REFERENCES episodes(id)
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
CREATE INDEX IF NOT EXISTS idx_changes_tenant ON change_log(tenant_id);
|
|
75
|
+
CREATE INDEX IF NOT EXISTS idx_changes_created ON change_log(created_at);
|
|
76
|
+
|
|
77
|
+
-- Agent registry
|
|
78
|
+
CREATE TABLE IF NOT EXISTS agents (
|
|
79
|
+
agent_id TEXT PRIMARY KEY,
|
|
80
|
+
tenant_id TEXT NOT NULL,
|
|
81
|
+
connected_at INTEGER NOT NULL,
|
|
82
|
+
last_sync_at INTEGER NOT NULL,
|
|
83
|
+
vector_clock TEXT NOT NULL
|
|
84
|
+
);
|
|
85
|
+
`);
|
|
86
|
+
logger.info('Federation hub database initialized');
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Start the hub server
|
|
90
|
+
*/
|
|
91
|
+
async start() {
|
|
92
|
+
return new Promise((resolve, reject) => {
|
|
93
|
+
try {
|
|
94
|
+
// Create HTTP server
|
|
95
|
+
this.server = createServer();
|
|
96
|
+
// Create WebSocket server
|
|
97
|
+
this.wss = new WebSocketServer({ server: this.server });
|
|
98
|
+
// Handle connections
|
|
99
|
+
this.wss.on('connection', (ws) => {
|
|
100
|
+
this.handleConnection(ws);
|
|
101
|
+
});
|
|
102
|
+
// Start listening
|
|
103
|
+
this.server.listen(this.config.port, () => {
|
|
104
|
+
logger.info('Federation hub server started', {
|
|
105
|
+
port: this.config.port,
|
|
106
|
+
protocol: 'WebSocket',
|
|
107
|
+
maxAgents: this.config.maxAgents
|
|
108
|
+
});
|
|
109
|
+
resolve();
|
|
110
|
+
});
|
|
111
|
+
// Error handling
|
|
112
|
+
this.server.on('error', (error) => {
|
|
113
|
+
logger.error('Hub server error', { error: error.message });
|
|
114
|
+
reject(error);
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
logger.error('Failed to start hub server', { error: error.message });
|
|
119
|
+
reject(error);
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Handle new agent connection
|
|
125
|
+
*/
|
|
126
|
+
handleConnection(ws) {
|
|
127
|
+
let agentId;
|
|
128
|
+
let tenantId;
|
|
129
|
+
let authenticated = false;
|
|
130
|
+
logger.info('New connection attempt');
|
|
131
|
+
ws.on('message', async (data) => {
|
|
132
|
+
try {
|
|
133
|
+
const message = JSON.parse(data.toString());
|
|
134
|
+
// Authentication required first
|
|
135
|
+
if (!authenticated && message.type !== 'auth') {
|
|
136
|
+
this.sendError(ws, 'Authentication required');
|
|
137
|
+
ws.close();
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
switch (message.type) {
|
|
141
|
+
case 'auth':
|
|
142
|
+
const authResult = await this.handleAuth(ws, message);
|
|
143
|
+
if (authResult) {
|
|
144
|
+
agentId = authResult.agentId;
|
|
145
|
+
tenantId = authResult.tenantId;
|
|
146
|
+
authenticated = true;
|
|
147
|
+
// Register connection
|
|
148
|
+
this.connections.set(agentId, {
|
|
149
|
+
ws,
|
|
150
|
+
agentId,
|
|
151
|
+
tenantId,
|
|
152
|
+
connectedAt: Date.now(),
|
|
153
|
+
lastSyncAt: Date.now(),
|
|
154
|
+
vectorClock: message.vectorClock || {}
|
|
155
|
+
});
|
|
156
|
+
logger.info('Agent authenticated', { agentId, tenantId });
|
|
157
|
+
}
|
|
158
|
+
break;
|
|
159
|
+
case 'pull':
|
|
160
|
+
if (agentId && tenantId) {
|
|
161
|
+
await this.handlePull(ws, agentId, tenantId, message);
|
|
162
|
+
}
|
|
163
|
+
break;
|
|
164
|
+
case 'push':
|
|
165
|
+
if (agentId && tenantId) {
|
|
166
|
+
await this.handlePush(ws, agentId, tenantId, message);
|
|
167
|
+
}
|
|
168
|
+
break;
|
|
169
|
+
default:
|
|
170
|
+
this.sendError(ws, `Unknown message type: ${message.type}`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
catch (error) {
|
|
174
|
+
logger.error('Message handling error', { error: error.message });
|
|
175
|
+
this.sendError(ws, error.message);
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
ws.on('close', () => {
|
|
179
|
+
if (agentId) {
|
|
180
|
+
this.connections.delete(agentId);
|
|
181
|
+
logger.info('Agent disconnected', { agentId, tenantId });
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
ws.on('error', (error) => {
|
|
185
|
+
logger.error('WebSocket error', { error: error.message, agentId });
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Handle authentication
|
|
190
|
+
*/
|
|
191
|
+
async handleAuth(ws, message) {
|
|
192
|
+
if (!message.agentId || !message.tenantId || !message.token) {
|
|
193
|
+
this.sendError(ws, 'Missing authentication credentials');
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
// TODO: Verify JWT token (for now, accept all)
|
|
197
|
+
// In production, verify JWT signature and expiration
|
|
198
|
+
// Register agent
|
|
199
|
+
this.db.prepare(`
|
|
200
|
+
INSERT OR REPLACE INTO agents (agent_id, tenant_id, connected_at, last_sync_at, vector_clock)
|
|
201
|
+
VALUES (?, ?, ?, ?, ?)
|
|
202
|
+
`).run(message.agentId, message.tenantId, Date.now(), Date.now(), JSON.stringify(message.vectorClock || {}));
|
|
203
|
+
// Send acknowledgment
|
|
204
|
+
this.send(ws, {
|
|
205
|
+
type: 'ack',
|
|
206
|
+
timestamp: Date.now()
|
|
207
|
+
});
|
|
208
|
+
return {
|
|
209
|
+
agentId: message.agentId,
|
|
210
|
+
tenantId: message.tenantId
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Handle pull request (agent wants updates from hub)
|
|
215
|
+
*/
|
|
216
|
+
async handlePull(ws, agentId, tenantId, message) {
|
|
217
|
+
const conn = this.connections.get(agentId);
|
|
218
|
+
if (!conn) {
|
|
219
|
+
this.sendError(ws, 'Agent not connected');
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
// Get changes since agent's last vector clock
|
|
223
|
+
const changes = await this.getChangesSince(tenantId, message.vectorClock || {});
|
|
224
|
+
// Update last sync time
|
|
225
|
+
conn.lastSyncAt = Date.now();
|
|
226
|
+
// Send changes to agent
|
|
227
|
+
this.send(ws, {
|
|
228
|
+
type: 'ack',
|
|
229
|
+
data: changes,
|
|
230
|
+
vectorClock: this.globalVectorClock,
|
|
231
|
+
timestamp: Date.now()
|
|
232
|
+
});
|
|
233
|
+
logger.info('Pull completed', {
|
|
234
|
+
agentId,
|
|
235
|
+
tenantId,
|
|
236
|
+
changeCount: changes.length
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Handle push request (agent sending updates to hub)
|
|
241
|
+
*/
|
|
242
|
+
async handlePush(ws, agentId, tenantId, message) {
|
|
243
|
+
const conn = this.connections.get(agentId);
|
|
244
|
+
if (!conn) {
|
|
245
|
+
this.sendError(ws, 'Agent not connected');
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
if (!message.data || message.data.length === 0) {
|
|
249
|
+
this.send(ws, { type: 'ack', timestamp: Date.now() });
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
// Store episodes in both SQLite (metadata) and AgentDB (vector memory)
|
|
253
|
+
const stmt = this.db.prepare(`
|
|
254
|
+
INSERT OR REPLACE INTO episodes
|
|
255
|
+
(tenant_id, agent_id, session_id, task, input, output, reward, critique, success, tokens_used, latency_ms, vector_clock, created_at)
|
|
256
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
257
|
+
`);
|
|
258
|
+
const changeStmt = this.db.prepare(`
|
|
259
|
+
INSERT INTO change_log (tenant_id, agent_id, operation, episode_id, vector_clock, created_at)
|
|
260
|
+
VALUES (?, ?, 'insert', last_insert_rowid(), ?, ?)
|
|
261
|
+
`);
|
|
262
|
+
let insertCount = 0;
|
|
263
|
+
for (const episode of message.data) {
|
|
264
|
+
try {
|
|
265
|
+
// Store in SQLite for metadata
|
|
266
|
+
stmt.run(tenantId, agentId, episode.sessionId || agentId, episode.task, episode.input, episode.output, episode.reward, episode.critique || '', episode.success ? 1 : 0, episode.tokensUsed || 0, episode.latencyMs || 0, JSON.stringify(message.vectorClock), Date.now());
|
|
267
|
+
changeStmt.run(tenantId, agentId, JSON.stringify(message.vectorClock), Date.now());
|
|
268
|
+
// Store in AgentDB for vector memory (with tenant isolation)
|
|
269
|
+
await this.agentDB.storePattern({
|
|
270
|
+
sessionId: `${tenantId}/${episode.sessionId || agentId}`,
|
|
271
|
+
task: episode.task,
|
|
272
|
+
input: episode.input,
|
|
273
|
+
output: episode.output,
|
|
274
|
+
reward: episode.reward,
|
|
275
|
+
critique: episode.critique || '',
|
|
276
|
+
success: episode.success,
|
|
277
|
+
tokensUsed: episode.tokensUsed || 0,
|
|
278
|
+
latencyMs: episode.latencyMs || 0,
|
|
279
|
+
metadata: {
|
|
280
|
+
tenantId,
|
|
281
|
+
agentId,
|
|
282
|
+
vectorClock: message.vectorClock
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
insertCount++;
|
|
286
|
+
}
|
|
287
|
+
catch (error) {
|
|
288
|
+
logger.error('Failed to insert episode', { error: error.message });
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
// Update global vector clock
|
|
292
|
+
if (message.vectorClock) {
|
|
293
|
+
for (const [agent, ts] of Object.entries(message.vectorClock)) {
|
|
294
|
+
this.globalVectorClock[agent] = Math.max(this.globalVectorClock[agent] || 0, ts);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
// Update connection vector clock
|
|
298
|
+
conn.vectorClock = { ...this.globalVectorClock };
|
|
299
|
+
conn.lastSyncAt = Date.now();
|
|
300
|
+
// Send acknowledgment
|
|
301
|
+
this.send(ws, {
|
|
302
|
+
type: 'ack',
|
|
303
|
+
timestamp: Date.now()
|
|
304
|
+
});
|
|
305
|
+
logger.info('Push completed', {
|
|
306
|
+
agentId,
|
|
307
|
+
tenantId,
|
|
308
|
+
episodeCount: insertCount
|
|
309
|
+
});
|
|
310
|
+
// Broadcast to other agents in same tenant (optional real-time sync)
|
|
311
|
+
this.broadcastToTenant(tenantId, agentId, {
|
|
312
|
+
type: 'push',
|
|
313
|
+
agentId,
|
|
314
|
+
data: message.data,
|
|
315
|
+
timestamp: Date.now()
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* Get changes since a given vector clock
|
|
320
|
+
* Returns memories from other agents in the same tenant
|
|
321
|
+
*/
|
|
322
|
+
async getChangesSince(tenantId, vectorClock) {
|
|
323
|
+
// Get all episodes for tenant from SQLite
|
|
324
|
+
const episodes = this.db.prepare(`
|
|
325
|
+
SELECT * FROM episodes
|
|
326
|
+
WHERE tenant_id = ?
|
|
327
|
+
ORDER BY created_at DESC
|
|
328
|
+
LIMIT 100
|
|
329
|
+
`).all(tenantId);
|
|
330
|
+
return episodes.map((row) => ({
|
|
331
|
+
id: row.id,
|
|
332
|
+
agentId: row.agent_id,
|
|
333
|
+
sessionId: row.session_id,
|
|
334
|
+
task: row.task,
|
|
335
|
+
input: row.input,
|
|
336
|
+
output: row.output,
|
|
337
|
+
reward: row.reward,
|
|
338
|
+
critique: row.critique,
|
|
339
|
+
success: row.success === 1,
|
|
340
|
+
tokensUsed: row.tokens_used,
|
|
341
|
+
latencyMs: row.latency_ms,
|
|
342
|
+
vectorClock: JSON.parse(row.vector_clock),
|
|
343
|
+
createdAt: row.created_at
|
|
344
|
+
}));
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* Broadcast message to all agents in a tenant (except sender)
|
|
348
|
+
*/
|
|
349
|
+
broadcastToTenant(tenantId, senderAgentId, message) {
|
|
350
|
+
let broadcastCount = 0;
|
|
351
|
+
for (const [agentId, conn] of this.connections.entries()) {
|
|
352
|
+
if (conn.tenantId === tenantId && agentId !== senderAgentId) {
|
|
353
|
+
this.send(conn.ws, message);
|
|
354
|
+
broadcastCount++;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
if (broadcastCount > 0) {
|
|
358
|
+
logger.debug('Broadcasted to tenant agents', {
|
|
359
|
+
tenantId,
|
|
360
|
+
recipientCount: broadcastCount
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* Send message to WebSocket
|
|
366
|
+
*/
|
|
367
|
+
send(ws, message) {
|
|
368
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
369
|
+
ws.send(JSON.stringify(message));
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* Send error message
|
|
374
|
+
*/
|
|
375
|
+
sendError(ws, error) {
|
|
376
|
+
this.send(ws, {
|
|
377
|
+
type: 'error',
|
|
378
|
+
error,
|
|
379
|
+
timestamp: Date.now()
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* Get hub statistics
|
|
384
|
+
*/
|
|
385
|
+
getStats() {
|
|
386
|
+
const totalEpisodes = this.db.prepare('SELECT COUNT(*) as count FROM episodes').get();
|
|
387
|
+
const tenants = this.db.prepare('SELECT COUNT(DISTINCT tenant_id) as count FROM episodes').get();
|
|
388
|
+
return {
|
|
389
|
+
connectedAgents: this.connections.size,
|
|
390
|
+
totalEpisodes: totalEpisodes.count,
|
|
391
|
+
tenants: tenants.count,
|
|
392
|
+
uptime: process.uptime()
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* Stop the hub server
|
|
397
|
+
*/
|
|
398
|
+
async stop() {
|
|
399
|
+
logger.info('Stopping federation hub server');
|
|
400
|
+
// Close all connections
|
|
401
|
+
for (const [agentId, conn] of this.connections.entries()) {
|
|
402
|
+
conn.ws.close();
|
|
403
|
+
}
|
|
404
|
+
this.connections.clear();
|
|
405
|
+
// Close WebSocket server
|
|
406
|
+
if (this.wss) {
|
|
407
|
+
this.wss.close();
|
|
408
|
+
}
|
|
409
|
+
// Close HTTP server
|
|
410
|
+
if (this.server) {
|
|
411
|
+
this.server.close();
|
|
412
|
+
}
|
|
413
|
+
// Close databases
|
|
414
|
+
this.db.close();
|
|
415
|
+
await this.agentDB.close();
|
|
416
|
+
logger.info('Federation hub server stopped');
|
|
417
|
+
}
|
|
418
|
+
/**
|
|
419
|
+
* Query patterns from AgentDB with tenant isolation
|
|
420
|
+
*/
|
|
421
|
+
async queryPatterns(tenantId, task, k = 5) {
|
|
422
|
+
try {
|
|
423
|
+
const results = await this.agentDB.searchPatterns({
|
|
424
|
+
task,
|
|
425
|
+
k,
|
|
426
|
+
minReward: 0.0
|
|
427
|
+
});
|
|
428
|
+
// Filter by tenant (session ID contains tenant prefix)
|
|
429
|
+
return results.filter((r) => r.sessionId?.startsWith(`${tenantId}/`));
|
|
430
|
+
}
|
|
431
|
+
catch (error) {
|
|
432
|
+
logger.error('Pattern query failed', { error: error.message });
|
|
433
|
+
return [];
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|