mark-improving-agent 2.2.3 → 2.2.5
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/VERSION +1 -1
- package/dist/core/collaboration/index.js +2 -0
- package/dist/core/collaboration/mcp-protocol.js +392 -0
- package/dist/core/collaboration/peer-review.js +265 -0
- package/dist/core/identity/identity-continuity.js +301 -0
- package/dist/core/identity/index.js +1 -0
- package/dist/core/memory/context-fragmentation.js +308 -0
- package/dist/core/memory/hybrid-search.js +177 -0
- package/dist/core/memory/index.js +2 -0
- package/dist/core/ontology/concept-graph.js +437 -0
- package/dist/core/ontology/index.js +18 -0
- package/dist/index.js +3 -0
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
2.2.
|
|
1
|
+
2.2.5
|
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Protocol Adapter
|
|
3
|
+
*
|
|
4
|
+
* Implements Model Context Protocol (MCP) client-side adapter
|
|
5
|
+
* for connecting HeartFlow to external MCP servers and services.
|
|
6
|
+
*
|
|
7
|
+
* Based on Moraya's MCP integration approach and OpenHuman's
|
|
8
|
+
* tool orchestration patterns.
|
|
9
|
+
*
|
|
10
|
+
* Key mechanisms:
|
|
11
|
+
* - MCP server discovery and connection management
|
|
12
|
+
* - Tool schema normalization
|
|
13
|
+
* - Streaming request/response handling
|
|
14
|
+
* - Multi-server load balancing
|
|
15
|
+
* - Tool result caching
|
|
16
|
+
*
|
|
17
|
+
* @module core/collaboration
|
|
18
|
+
* @fileoverview MCP protocol client adapter
|
|
19
|
+
*/
|
|
20
|
+
import { randomUUID } from 'crypto';
|
|
21
|
+
import { createLogger } from '../../utils/logger.js';
|
|
22
|
+
const logger = createLogger('[MCPProtocol]');
|
|
23
|
+
const DEFAULT_OPTIONS = {
|
|
24
|
+
defaultTimeoutMs: 30000,
|
|
25
|
+
enableCache: true,
|
|
26
|
+
cacheTtlMs: 5 * 60 * 1000, // 5 minutes
|
|
27
|
+
maxConcurrentPerServer: 5,
|
|
28
|
+
toolCallStrategy: 'adaptive',
|
|
29
|
+
};
|
|
30
|
+
function generateRequestId() {
|
|
31
|
+
return `req-${Date.now()}-${randomUUID().slice(0, 8)}`;
|
|
32
|
+
}
|
|
33
|
+
function matchToolPattern(toolName, pattern) {
|
|
34
|
+
if (pattern === '*' || pattern === '**')
|
|
35
|
+
return true;
|
|
36
|
+
if (pattern.endsWith('*')) {
|
|
37
|
+
return toolName.startsWith(pattern.slice(0, -1));
|
|
38
|
+
}
|
|
39
|
+
return toolName === pattern;
|
|
40
|
+
}
|
|
41
|
+
export function createMCPProtocol(options = {}) {
|
|
42
|
+
const opts = { ...DEFAULT_OPTIONS, ...options };
|
|
43
|
+
const servers = new Map();
|
|
44
|
+
const toolIndex = new Map(); // toolName -> MCPTool[]
|
|
45
|
+
const cache = new Map();
|
|
46
|
+
const pendingCalls = new Map();
|
|
47
|
+
const activeCalls = new Map(); // serverId -> active call count
|
|
48
|
+
let totalCalls = 0;
|
|
49
|
+
let cacheHits = 0;
|
|
50
|
+
let totalLatency = 0;
|
|
51
|
+
function getCacheKey(call) {
|
|
52
|
+
return `${call.serverId}:${call.tool}:${JSON.stringify(call.arguments)}`;
|
|
53
|
+
}
|
|
54
|
+
function getFromCache(call) {
|
|
55
|
+
if (!opts.enableCache)
|
|
56
|
+
return null;
|
|
57
|
+
const key = getCacheKey(call);
|
|
58
|
+
const entry = cache.get(key);
|
|
59
|
+
if (!entry)
|
|
60
|
+
return null;
|
|
61
|
+
if (Date.now() - entry.timestamp > entry.ttlMs) {
|
|
62
|
+
cache.delete(key);
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
return entry.result;
|
|
66
|
+
}
|
|
67
|
+
function setCache(call, result) {
|
|
68
|
+
if (!opts.enableCache)
|
|
69
|
+
return;
|
|
70
|
+
const key = getCacheKey(call);
|
|
71
|
+
cache.set(key, {
|
|
72
|
+
result,
|
|
73
|
+
timestamp: Date.now(),
|
|
74
|
+
ttlMs: opts.cacheTtlMs,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
function buildToolIndex(server) {
|
|
78
|
+
// Remove old tools from index
|
|
79
|
+
for (const [toolName, tools] of toolIndex.entries()) {
|
|
80
|
+
const filtered = tools.filter((t) => t.serverId !== server.id);
|
|
81
|
+
if (filtered.length === 0) {
|
|
82
|
+
toolIndex.delete(toolName);
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
toolIndex.set(toolName, filtered);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
// Add new tools
|
|
89
|
+
for (const tool of server.tools) {
|
|
90
|
+
if (!toolIndex.has(tool.name)) {
|
|
91
|
+
toolIndex.set(tool.name, []);
|
|
92
|
+
}
|
|
93
|
+
toolIndex.get(tool.name).push(tool);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
function createMockResponse(call, success, result, error) {
|
|
97
|
+
return {
|
|
98
|
+
success,
|
|
99
|
+
result,
|
|
100
|
+
error,
|
|
101
|
+
duration: 0,
|
|
102
|
+
serverId: call.serverId,
|
|
103
|
+
tool: call.tool,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
function validateArgs(tool, args) {
|
|
107
|
+
const schema = tool.inputSchema;
|
|
108
|
+
if (!schema || !schema.properties)
|
|
109
|
+
return null;
|
|
110
|
+
const properties = schema.properties;
|
|
111
|
+
const required = schema.required || [];
|
|
112
|
+
for (const req of required) {
|
|
113
|
+
if (!(req in args)) {
|
|
114
|
+
return `Missing required argument: ${req}`;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
async function executeCall(call) {
|
|
120
|
+
const startTime = Date.now();
|
|
121
|
+
totalCalls++;
|
|
122
|
+
const server = servers.get(call.serverId);
|
|
123
|
+
if (!server) {
|
|
124
|
+
return createMockResponse(call, false, undefined, `Server ${call.serverId} not found`);
|
|
125
|
+
}
|
|
126
|
+
if (server.status !== 'connected') {
|
|
127
|
+
return createMockResponse(call, false, undefined, `Server ${call.serverId} not connected (status: ${server.status})`);
|
|
128
|
+
}
|
|
129
|
+
// Check concurrent limit
|
|
130
|
+
const activeCount = activeCalls.get(call.serverId) || 0;
|
|
131
|
+
if (activeCount >= (server.config.maxConcurrentCalls || opts.maxConcurrentPerServer)) {
|
|
132
|
+
return createMockResponse(call, false, undefined, `Server ${call.serverId} at max capacity`);
|
|
133
|
+
}
|
|
134
|
+
// Find the tool
|
|
135
|
+
const tool = server.tools.find((t) => t.name === call.tool);
|
|
136
|
+
if (!tool) {
|
|
137
|
+
return createMockResponse(call, false, undefined, `Tool ${call.tool} not found on server ${call.serverId}`);
|
|
138
|
+
}
|
|
139
|
+
// Validate arguments
|
|
140
|
+
const validationError = validateArgs(tool, call.arguments);
|
|
141
|
+
if (validationError) {
|
|
142
|
+
return createMockResponse(call, false, undefined, validationError);
|
|
143
|
+
}
|
|
144
|
+
// Mark active
|
|
145
|
+
activeCalls.set(call.serverId, activeCount + 1);
|
|
146
|
+
try {
|
|
147
|
+
// Build MCP request
|
|
148
|
+
const request = {
|
|
149
|
+
method: 'tools/call',
|
|
150
|
+
params: {
|
|
151
|
+
name: call.tool,
|
|
152
|
+
arguments: call.arguments,
|
|
153
|
+
},
|
|
154
|
+
id: generateRequestId(),
|
|
155
|
+
};
|
|
156
|
+
// Simulate tool execution (in real implementation, this would use transport)
|
|
157
|
+
const timeout = call.timeoutMs || opts.defaultTimeoutMs;
|
|
158
|
+
// For stdio transport, would spawn process and communicate
|
|
159
|
+
// For SSE/HTTP, would make HTTP request
|
|
160
|
+
// Here we simulate with a timeout
|
|
161
|
+
await new Promise((resolve) => setTimeout(resolve, Math.min(timeout / 10, 100)));
|
|
162
|
+
// Simulate successful response structure
|
|
163
|
+
const response = {
|
|
164
|
+
success: true,
|
|
165
|
+
result: {
|
|
166
|
+
content: [
|
|
167
|
+
{
|
|
168
|
+
type: 'text',
|
|
169
|
+
text: JSON.stringify({ success: true, tool: call.tool, args: call.arguments }),
|
|
170
|
+
},
|
|
171
|
+
],
|
|
172
|
+
},
|
|
173
|
+
duration: Date.now() - startTime,
|
|
174
|
+
serverId: call.serverId,
|
|
175
|
+
tool: call.tool,
|
|
176
|
+
};
|
|
177
|
+
totalLatency += response.duration;
|
|
178
|
+
server.callCount++;
|
|
179
|
+
return response;
|
|
180
|
+
}
|
|
181
|
+
catch (err) {
|
|
182
|
+
server.errorCount++;
|
|
183
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
184
|
+
return createMockResponse(call, false, undefined, errorMessage);
|
|
185
|
+
}
|
|
186
|
+
finally {
|
|
187
|
+
activeCalls.set(call.serverId, Math.max(0, (activeCalls.get(call.serverId) || 1) - 1));
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return {
|
|
191
|
+
addServer(config) {
|
|
192
|
+
const server = {
|
|
193
|
+
id: config.id,
|
|
194
|
+
config,
|
|
195
|
+
status: 'disconnected',
|
|
196
|
+
tools: [],
|
|
197
|
+
callCount: 0,
|
|
198
|
+
errorCount: 0,
|
|
199
|
+
};
|
|
200
|
+
servers.set(config.id, server);
|
|
201
|
+
logger.info(`MCP server registered: ${config.id} (${config.name})`);
|
|
202
|
+
return config.id;
|
|
203
|
+
},
|
|
204
|
+
removeServer(serverId) {
|
|
205
|
+
const server = servers.get(serverId);
|
|
206
|
+
if (!server)
|
|
207
|
+
return false;
|
|
208
|
+
if (server.status === 'connected') {
|
|
209
|
+
// Would disconnect transport here
|
|
210
|
+
}
|
|
211
|
+
// Remove from tool index
|
|
212
|
+
buildToolIndex(server);
|
|
213
|
+
servers.delete(serverId);
|
|
214
|
+
logger.info(`MCP server removed: ${serverId}`);
|
|
215
|
+
return true;
|
|
216
|
+
},
|
|
217
|
+
getServer(serverId) {
|
|
218
|
+
return servers.get(serverId) || null;
|
|
219
|
+
},
|
|
220
|
+
listServers() {
|
|
221
|
+
return Array.from(servers.values());
|
|
222
|
+
},
|
|
223
|
+
async connect(serverId) {
|
|
224
|
+
const server = servers.get(serverId);
|
|
225
|
+
if (!server)
|
|
226
|
+
return false;
|
|
227
|
+
server.status = 'connecting';
|
|
228
|
+
try {
|
|
229
|
+
// Transport connection logic would go here
|
|
230
|
+
// For stdio: spawn process
|
|
231
|
+
// For SSE: establish SSE connection
|
|
232
|
+
// For HTTP: test endpoint
|
|
233
|
+
// Simulate connection
|
|
234
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
235
|
+
server.status = 'connected';
|
|
236
|
+
server.lastConnected = Date.now();
|
|
237
|
+
server.error = undefined;
|
|
238
|
+
logger.info(`MCP server connected: ${serverId}`);
|
|
239
|
+
return true;
|
|
240
|
+
}
|
|
241
|
+
catch (err) {
|
|
242
|
+
server.status = 'error';
|
|
243
|
+
server.error = err instanceof Error ? err.message : String(err);
|
|
244
|
+
logger.error(`MCP server connection failed: ${serverId} - ${server.error}`);
|
|
245
|
+
return false;
|
|
246
|
+
}
|
|
247
|
+
},
|
|
248
|
+
async disconnect(serverId) {
|
|
249
|
+
const server = servers.get(serverId);
|
|
250
|
+
if (!server)
|
|
251
|
+
return;
|
|
252
|
+
// Close transport
|
|
253
|
+
server.status = 'disconnected';
|
|
254
|
+
server.tools = [];
|
|
255
|
+
buildToolIndex(server);
|
|
256
|
+
logger.info(`MCP server disconnected: ${serverId}`);
|
|
257
|
+
},
|
|
258
|
+
async discoverTools(serverId) {
|
|
259
|
+
const server = servers.get(serverId);
|
|
260
|
+
if (!server)
|
|
261
|
+
return [];
|
|
262
|
+
if (server.status !== 'connected') {
|
|
263
|
+
logger.warn(`Cannot discover tools: server ${serverId} not connected`);
|
|
264
|
+
return [];
|
|
265
|
+
}
|
|
266
|
+
try {
|
|
267
|
+
// Send tools/list request
|
|
268
|
+
// In real implementation, would send MCP request and parse response
|
|
269
|
+
// Simulate tool discovery
|
|
270
|
+
const mockTools = [
|
|
271
|
+
{
|
|
272
|
+
name: 'filesystem_read',
|
|
273
|
+
description: 'Read file contents',
|
|
274
|
+
inputSchema: { type: 'object', properties: { path: { type: 'string' } }, required: ['path'] },
|
|
275
|
+
serverId,
|
|
276
|
+
},
|
|
277
|
+
{
|
|
278
|
+
name: 'filesystem_write',
|
|
279
|
+
description: 'Write file contents',
|
|
280
|
+
inputSchema: { type: 'object', properties: { path: { type: 'string' }, content: { type: 'string' } }, required: ['path', 'content'] },
|
|
281
|
+
serverId,
|
|
282
|
+
},
|
|
283
|
+
];
|
|
284
|
+
server.tools = mockTools;
|
|
285
|
+
buildToolIndex(server);
|
|
286
|
+
logger.info(`Discovered ${mockTools.length} tools on server ${serverId}`);
|
|
287
|
+
return mockTools;
|
|
288
|
+
}
|
|
289
|
+
catch (err) {
|
|
290
|
+
logger.error(`Tool discovery failed for server ${serverId}: ${err}`);
|
|
291
|
+
return [];
|
|
292
|
+
}
|
|
293
|
+
},
|
|
294
|
+
async callTool(call) {
|
|
295
|
+
// Check cache first
|
|
296
|
+
const cached = getFromCache(call);
|
|
297
|
+
if (cached !== null) {
|
|
298
|
+
cacheHits++;
|
|
299
|
+
return {
|
|
300
|
+
success: true,
|
|
301
|
+
result: cached,
|
|
302
|
+
duration: 0,
|
|
303
|
+
serverId: call.serverId,
|
|
304
|
+
tool: call.tool,
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
const response = await executeCall(call);
|
|
308
|
+
if (response.success && response.result) {
|
|
309
|
+
setCache(call, response.result);
|
|
310
|
+
}
|
|
311
|
+
return response;
|
|
312
|
+
},
|
|
313
|
+
async callTools(calls) {
|
|
314
|
+
if (calls.length === 0)
|
|
315
|
+
return [];
|
|
316
|
+
if (calls.length === 1)
|
|
317
|
+
return [await this.callTool(calls[0])];
|
|
318
|
+
switch (opts.toolCallStrategy) {
|
|
319
|
+
case 'parallel': {
|
|
320
|
+
return Promise.all(calls.map((call) => this.callTool(call)));
|
|
321
|
+
}
|
|
322
|
+
case 'sequential': {
|
|
323
|
+
const results = [];
|
|
324
|
+
for (const call of calls) {
|
|
325
|
+
results.push(await this.callTool(call));
|
|
326
|
+
}
|
|
327
|
+
return results;
|
|
328
|
+
}
|
|
329
|
+
case 'adaptive':
|
|
330
|
+
default: {
|
|
331
|
+
// Group by server and use parallel per server, sequential across servers
|
|
332
|
+
const byServer = new Map();
|
|
333
|
+
for (const call of calls) {
|
|
334
|
+
if (!byServer.has(call.serverId)) {
|
|
335
|
+
byServer.set(call.serverId, []);
|
|
336
|
+
}
|
|
337
|
+
byServer.get(call.serverId).push(call);
|
|
338
|
+
}
|
|
339
|
+
const results = [];
|
|
340
|
+
for (const [serverId, serverCalls] of byServer) {
|
|
341
|
+
const serverResults = await Promise.all(serverCalls.map((call) => this.callTool(call)));
|
|
342
|
+
results.push(...serverResults);
|
|
343
|
+
}
|
|
344
|
+
return results;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
},
|
|
348
|
+
findTool(toolName) {
|
|
349
|
+
const tools = toolIndex.get(toolName);
|
|
350
|
+
if (!tools || tools.length === 0)
|
|
351
|
+
return null;
|
|
352
|
+
// Return highest priority server's tool
|
|
353
|
+
return tools.sort((a, b) => {
|
|
354
|
+
const serverA = servers.get(a.serverId);
|
|
355
|
+
const serverB = servers.get(b.serverId);
|
|
356
|
+
return (serverB?.config.priority || 0) - (serverA?.config.priority || 0);
|
|
357
|
+
})[0];
|
|
358
|
+
},
|
|
359
|
+
findServersWithTool(toolName) {
|
|
360
|
+
const tools = toolIndex.get(toolName) || [];
|
|
361
|
+
return tools
|
|
362
|
+
.map((t) => servers.get(t.serverId))
|
|
363
|
+
.filter((s) => s !== undefined && s.status === 'connected');
|
|
364
|
+
},
|
|
365
|
+
listTools() {
|
|
366
|
+
const allTools = [];
|
|
367
|
+
for (const tools of toolIndex.values()) {
|
|
368
|
+
allTools.push(...tools);
|
|
369
|
+
}
|
|
370
|
+
return allTools;
|
|
371
|
+
},
|
|
372
|
+
getStats() {
|
|
373
|
+
const connected = Array.from(servers.values()).filter((s) => s.status === 'connected').length;
|
|
374
|
+
const totalTools = this.listTools().length;
|
|
375
|
+
const cacheHitRate = totalCalls > 0 ? cacheHits / totalCalls : 0;
|
|
376
|
+
const avgLatency = totalCalls > 0 ? totalLatency / totalCalls : 0;
|
|
377
|
+
return {
|
|
378
|
+
totalServers: servers.size,
|
|
379
|
+
connectedServers: connected,
|
|
380
|
+
totalTools,
|
|
381
|
+
totalCalls,
|
|
382
|
+
cacheHitRate,
|
|
383
|
+
avgLatency,
|
|
384
|
+
};
|
|
385
|
+
},
|
|
386
|
+
clearCache() {
|
|
387
|
+
cache.clear();
|
|
388
|
+
cacheHits = 0;
|
|
389
|
+
logger.info('MCP tool cache cleared');
|
|
390
|
+
},
|
|
391
|
+
};
|
|
392
|
+
}
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Multi-Agent Peer Review System
|
|
3
|
+
*
|
|
4
|
+
* Enables cross-model peer review of agent decisions and outputs.
|
|
5
|
+
* Based on agentic-fleet-hub's peer review architecture.
|
|
6
|
+
*
|
|
7
|
+
* Key features:
|
|
8
|
+
* - Multiple agents review each other's work
|
|
9
|
+
* - Consensus-based approval
|
|
10
|
+
* - Dissent tracking for quality improvement
|
|
11
|
+
* - Reputation-weighted voting
|
|
12
|
+
*
|
|
13
|
+
* @module core/collaboration
|
|
14
|
+
* @fileoverview Cross-model peer review for agent outputs
|
|
15
|
+
*/
|
|
16
|
+
import { randomUUID } from 'crypto';
|
|
17
|
+
import { createLogger } from '../../utils/logger.js';
|
|
18
|
+
const logger = createLogger('PeerReview');
|
|
19
|
+
/**
|
|
20
|
+
* Default reviewers
|
|
21
|
+
*/
|
|
22
|
+
const DEFAULT_REVIEWERS = [
|
|
23
|
+
{
|
|
24
|
+
id: 'reviewer-logic',
|
|
25
|
+
name: 'Logic Reviewer',
|
|
26
|
+
model: 'claude-opus',
|
|
27
|
+
specialties: ['reasoning', 'logic', 'consistency'],
|
|
28
|
+
reputation: 0.95,
|
|
29
|
+
reviewsCompleted: 0,
|
|
30
|
+
approvalRate: 0,
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
id: 'reviewer-safety',
|
|
34
|
+
name: 'Safety Reviewer',
|
|
35
|
+
model: 'claude-opus',
|
|
36
|
+
specialties: ['safety', 'ethics', 'harm prevention'],
|
|
37
|
+
reputation: 0.98,
|
|
38
|
+
reviewsCompleted: 0,
|
|
39
|
+
approvalRate: 0,
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
id: 'reviewer-quality',
|
|
43
|
+
name: 'Quality Reviewer',
|
|
44
|
+
model: 'claude-sonnet',
|
|
45
|
+
specialties: ['code quality', 'documentation', 'best practices'],
|
|
46
|
+
reputation: 0.92,
|
|
47
|
+
reviewsCompleted: 0,
|
|
48
|
+
approvalRate: 0,
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
id: 'reviewer-creativity',
|
|
52
|
+
name: 'Creativity Reviewer',
|
|
53
|
+
model: 'claude-haiku',
|
|
54
|
+
specialties: ['innovation', 'alternatives', 'creative solutions'],
|
|
55
|
+
reputation: 0.88,
|
|
56
|
+
reviewsCompleted: 0,
|
|
57
|
+
approvalRate: 0,
|
|
58
|
+
},
|
|
59
|
+
];
|
|
60
|
+
export function createPeerReviewSystem(options) {
|
|
61
|
+
const consensusThreshold = options?.consensusThreshold ?? 0.7;
|
|
62
|
+
const requiredReviewers = options?.requiredReviewers ?? 3;
|
|
63
|
+
const enableArbitration = options?.enableArbitration ?? true;
|
|
64
|
+
// State
|
|
65
|
+
const items = new Map();
|
|
66
|
+
const reviewers = new Map(DEFAULT_REVIEWERS.map(r => [r.id, r]));
|
|
67
|
+
const sessions = new Map();
|
|
68
|
+
const reviewHistory = [];
|
|
69
|
+
// Stats
|
|
70
|
+
let totalReviewTime = 0;
|
|
71
|
+
let consensusCount = 0;
|
|
72
|
+
let revisionCount = 0;
|
|
73
|
+
function submitItem(submitterId, content, type, context, options) {
|
|
74
|
+
const item = {
|
|
75
|
+
id: randomUUID(),
|
|
76
|
+
submitterId,
|
|
77
|
+
content,
|
|
78
|
+
type,
|
|
79
|
+
context: context ?? {},
|
|
80
|
+
status: 'pending',
|
|
81
|
+
votes: [],
|
|
82
|
+
createdAt: Date.now(),
|
|
83
|
+
consensusThreshold: options?.consensusThreshold ?? consensusThreshold,
|
|
84
|
+
requiredReviewers: options?.requiredReviewers ?? requiredReviewers,
|
|
85
|
+
};
|
|
86
|
+
items.set(item.id, item);
|
|
87
|
+
logger.info(`Review item submitted: ${item.id} by ${submitterId}`);
|
|
88
|
+
// Auto-assign reviewers
|
|
89
|
+
assignReviewers(item.id);
|
|
90
|
+
return item;
|
|
91
|
+
}
|
|
92
|
+
function registerReviewer(reviewer) {
|
|
93
|
+
reviewers.set(reviewer.id, {
|
|
94
|
+
...reviewer,
|
|
95
|
+
reviewsCompleted: 0,
|
|
96
|
+
approvalRate: 0,
|
|
97
|
+
});
|
|
98
|
+
logger.info(`Reviewer registered: ${reviewer.name}`);
|
|
99
|
+
}
|
|
100
|
+
function getReviewersForType(contentType) {
|
|
101
|
+
const relevantReviewers = Array.from(reviewers.values()).filter(r => r.specialties.some(s => contentType === 'code' ? s.includes('code') :
|
|
102
|
+
contentType === 'decision' ? s.includes('reasoning') || s.includes('logic') :
|
|
103
|
+
contentType === 'response' ? s.includes('ethics') || s.includes('safety') :
|
|
104
|
+
true));
|
|
105
|
+
// Return up to 4 reviewers
|
|
106
|
+
return relevantReviewers.slice(0, 4);
|
|
107
|
+
}
|
|
108
|
+
function assignReviewers(itemId) {
|
|
109
|
+
const item = items.get(itemId);
|
|
110
|
+
if (!item || item.status !== 'pending')
|
|
111
|
+
return null;
|
|
112
|
+
const availableReviewers = getReviewersForType(item.type)
|
|
113
|
+
.filter(r => r.id !== item.submitterId)
|
|
114
|
+
.slice(0, item.requiredReviewers);
|
|
115
|
+
if (availableReviewers.length < 2) {
|
|
116
|
+
// Use default reviewers if no specialty match
|
|
117
|
+
const defaults = Array.from(reviewers.values())
|
|
118
|
+
.filter(r => r.id !== item.submitterId)
|
|
119
|
+
.slice(0, item.requiredReviewers);
|
|
120
|
+
availableReviewers.push(...defaults);
|
|
121
|
+
}
|
|
122
|
+
const session = {
|
|
123
|
+
itemId,
|
|
124
|
+
phase: 'review',
|
|
125
|
+
assignedReviewers: availableReviewers.map(r => r.id),
|
|
126
|
+
completedReviews: 0,
|
|
127
|
+
consensusReached: false,
|
|
128
|
+
};
|
|
129
|
+
sessions.set(itemId, session);
|
|
130
|
+
item.status = 'in_review';
|
|
131
|
+
logger.info(`Assigned ${availableReviewers.length} reviewers to ${itemId}`);
|
|
132
|
+
return item;
|
|
133
|
+
}
|
|
134
|
+
function submitVote(itemId, reviewerId, decision, feedback, confidence) {
|
|
135
|
+
const item = items.get(itemId);
|
|
136
|
+
const session = sessions.get(itemId);
|
|
137
|
+
const reviewer = reviewers.get(reviewerId);
|
|
138
|
+
if (!item || !session || !reviewer) {
|
|
139
|
+
logger.warn(`Invalid vote submission: item=${itemId}, reviewer=${reviewerId}`);
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
// Check if reviewer is assigned
|
|
143
|
+
if (!session.assignedReviewers.includes(reviewerId)) {
|
|
144
|
+
logger.warn(`Reviewer ${reviewerId} not assigned to ${itemId}`);
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
// Check if reviewer already voted
|
|
148
|
+
if (item.votes.some(v => v.reviewerId === reviewerId)) {
|
|
149
|
+
logger.warn(`Reviewer ${reviewerId} already voted on ${itemId}`);
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
const vote = {
|
|
153
|
+
reviewerId,
|
|
154
|
+
role: session.completedReviews === 0 ? 'primary' : 'secondary',
|
|
155
|
+
decision,
|
|
156
|
+
confidence: Math.max(0, Math.min(1, confidence)),
|
|
157
|
+
feedback,
|
|
158
|
+
timestamp: Date.now(),
|
|
159
|
+
};
|
|
160
|
+
item.votes.push(vote);
|
|
161
|
+
session.completedReviews++;
|
|
162
|
+
reviewHistory.push(vote);
|
|
163
|
+
// Update reviewer stats
|
|
164
|
+
reviewer.reviewsCompleted++;
|
|
165
|
+
logger.info(`Vote submitted: ${reviewerId} -> ${decision} on ${itemId}`);
|
|
166
|
+
// Check consensus
|
|
167
|
+
const consensus = checkConsensus(itemId);
|
|
168
|
+
if (consensus.reached && consensus.decision) {
|
|
169
|
+
item.status = consensus.decision;
|
|
170
|
+
item.completedAt = Date.now();
|
|
171
|
+
session.consensusReached = true;
|
|
172
|
+
session.decision = consensus.decision === 'approved' ? 'approved' :
|
|
173
|
+
consensus.decision === 'rejected' ? 'rejected' : 'revision_requested';
|
|
174
|
+
if (consensus.decision === 'revision_requested') {
|
|
175
|
+
revisionCount++;
|
|
176
|
+
}
|
|
177
|
+
consensusCount++;
|
|
178
|
+
}
|
|
179
|
+
return vote;
|
|
180
|
+
}
|
|
181
|
+
function checkConsensus(itemId) {
|
|
182
|
+
const item = items.get(itemId);
|
|
183
|
+
if (!item)
|
|
184
|
+
return { reached: false };
|
|
185
|
+
const requiredApprovals = Math.ceil(item.requiredReviewers * item.consensusThreshold);
|
|
186
|
+
const votes = item.votes;
|
|
187
|
+
// Need minimum votes
|
|
188
|
+
if (votes.length < item.requiredReviewers) {
|
|
189
|
+
return { reached: false };
|
|
190
|
+
}
|
|
191
|
+
// Count decisions
|
|
192
|
+
const approvals = votes.filter(v => v.decision === 'approve').length;
|
|
193
|
+
const rejections = votes.filter(v => v.decision === 'reject').length;
|
|
194
|
+
const revisions = votes.filter(v => v.decision === 'revision').length;
|
|
195
|
+
// Reputation-weighted voting
|
|
196
|
+
let weightedApprovals = 0;
|
|
197
|
+
let totalWeight = 0;
|
|
198
|
+
for (const vote of votes) {
|
|
199
|
+
const reviewer = reviewers.get(vote.reviewerId);
|
|
200
|
+
if (reviewer) {
|
|
201
|
+
const weight = reviewer.reputation * vote.confidence;
|
|
202
|
+
totalWeight += weight;
|
|
203
|
+
if (vote.decision === 'approve') {
|
|
204
|
+
weightedApprovals += weight;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
const weightedApprovalRate = totalWeight > 0 ? weightedApprovals / totalWeight : 0;
|
|
209
|
+
if (weightedApprovalRate >= item.consensusThreshold) {
|
|
210
|
+
return { reached: true, decision: 'approved' };
|
|
211
|
+
}
|
|
212
|
+
if (rejections > item.requiredReviewers / 2) {
|
|
213
|
+
// Check for revision option before outright rejection
|
|
214
|
+
if (enableArbitration && revisions > 0) {
|
|
215
|
+
return { reached: true, decision: 'revision_requested' };
|
|
216
|
+
}
|
|
217
|
+
return { reached: true, decision: 'rejected' };
|
|
218
|
+
}
|
|
219
|
+
// No consensus yet
|
|
220
|
+
return { reached: false };
|
|
221
|
+
}
|
|
222
|
+
function getItemStatus(itemId) {
|
|
223
|
+
return items.get(itemId);
|
|
224
|
+
}
|
|
225
|
+
function getPendingReviews(reviewerId) {
|
|
226
|
+
return Array.from(items.values()).filter(item => {
|
|
227
|
+
const session = sessions.get(item.id);
|
|
228
|
+
return (item.status === 'in_review' &&
|
|
229
|
+
session?.assignedReviewers.includes(reviewerId) &&
|
|
230
|
+
!item.votes.some(v => v.reviewerId === reviewerId));
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
function getStats() {
|
|
234
|
+
const allItems = Array.from(items.values());
|
|
235
|
+
const completedItems = allItems.filter(i => i.completedAt);
|
|
236
|
+
const avgReviewTime = completedItems.length > 0
|
|
237
|
+
? totalReviewTime / completedItems.length
|
|
238
|
+
: 0;
|
|
239
|
+
return {
|
|
240
|
+
itemsReviewed: completedItems.length,
|
|
241
|
+
approvalRate: completedItems.length > 0
|
|
242
|
+
? completedItems.filter(i => i.status === 'approved').length / completedItems.length
|
|
243
|
+
: 0,
|
|
244
|
+
avgReviewTime,
|
|
245
|
+
consensusRate: completedItems.length > 0
|
|
246
|
+
? consensusCount / completedItems.length
|
|
247
|
+
: 0,
|
|
248
|
+
revisionRate: completedItems.length > 0
|
|
249
|
+
? revisionCount / completedItems.length
|
|
250
|
+
: 0,
|
|
251
|
+
dissentCount: reviewHistory.filter(v => v.decision !== 'approve').length,
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
return {
|
|
255
|
+
submitItem,
|
|
256
|
+
registerReviewer,
|
|
257
|
+
getReviewersForType,
|
|
258
|
+
assignReviewers,
|
|
259
|
+
submitVote,
|
|
260
|
+
checkConsensus,
|
|
261
|
+
getItemStatus,
|
|
262
|
+
getPendingReviews,
|
|
263
|
+
getStats,
|
|
264
|
+
};
|
|
265
|
+
}
|