mark-improving-agent 2.2.4 → 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 CHANGED
@@ -1 +1 @@
1
- 2.2.4
1
+ 2.2.5
@@ -2,3 +2,4 @@ export * from './multi-agent.js';
2
2
  export * from './agentic-loop.js';
3
3
  export * from './multi-agent-system.js';
4
4
  export { createMCPProtocol } from './mcp-protocol.js';
5
+ export { createPeerReviewSystem } from './peer-review.js';
@@ -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
+ }
@@ -0,0 +1,177 @@
1
+ /**
2
+ * Hybrid Memory Search
3
+ *
4
+ * Combines vector similarity search with BM25 keyword matching and knowledge graph traversal
5
+ * Based on Dakera AI's hybrid search architecture (87.8% LoCoMo accuracy)
6
+ *
7
+ * @module core/memory
8
+ * @fileoverview Hybrid search combining vector + BM25 + knowledge graph for superior recall
9
+ */
10
+ import { createLogger } from '../../utils/logger.js';
11
+ const logger = createLogger('HybridSearch');
12
+ /**
13
+ * Default hybrid search configuration
14
+ */
15
+ export const DEFAULT_HYBRID_CONFIG = {
16
+ vectorWeight: 0.5,
17
+ bm25Weight: 0.3,
18
+ kgWeight: 0.2,
19
+ bm25: { k1: 1.5, b: 0.75 },
20
+ minScore: 0.1,
21
+ maxResults: 20,
22
+ };
23
+ /**
24
+ * Tokenizer for BM25
25
+ */
26
+ function tokenize(text) {
27
+ return text
28
+ .toLowerCase()
29
+ .replace(/[^\w\s]/g, ' ')
30
+ .split(/\s+/)
31
+ .filter(token => token.length > 2);
32
+ }
33
+ /**
34
+ * Create a BM25 index from memory entries
35
+ */
36
+ export function createBM25Index(entries) {
37
+ const index = {
38
+ docLengths: new Map(),
39
+ termDocFreq: new Map(),
40
+ avgDocLength: 0,
41
+ totalDocs: entries.length,
42
+ invertedIndex: new Map(),
43
+ };
44
+ let totalLength = 0;
45
+ for (const entry of entries) {
46
+ const tokens = tokenize(entry.content);
47
+ const docLength = tokens.length;
48
+ index.docLengths.set(entry.id, docLength);
49
+ totalLength += docLength;
50
+ // Count term frequencies
51
+ const termFreq = new Map();
52
+ for (const token of tokens) {
53
+ termFreq.set(token, (termFreq.get(token) ?? 0) + 1);
54
+ if (!index.invertedIndex.has(token)) {
55
+ index.invertedIndex.set(token, new Map());
56
+ }
57
+ const posting = index.invertedIndex.get(token);
58
+ posting.set(entry.id, termFreq.get(token));
59
+ }
60
+ // Update document frequency
61
+ for (const token of new Set(tokens)) {
62
+ index.termDocFreq.set(token, (index.termDocFreq.get(token) ?? 0) + 1);
63
+ }
64
+ }
65
+ index.avgDocLength = totalLength / Math.max(entries.length, 1);
66
+ return index;
67
+ }
68
+ /**
69
+ * Calculate BM25 score for a single document
70
+ */
71
+ export function bm25Score(index, docId, queryTokens, config) {
72
+ const docLength = index.docLengths.get(docId) ?? 0;
73
+ let score = 0;
74
+ for (const token of queryTokens) {
75
+ const tf = index.invertedIndex.get(token)?.get(docId) ?? 0;
76
+ if (tf === 0)
77
+ continue;
78
+ const df = index.termDocFreq.get(token) ?? 0;
79
+ if (df === 0)
80
+ continue;
81
+ const idf = Math.log((index.totalDocs - df + 0.5) / (df + 0.5) + 1);
82
+ const tfComponent = (tf * (config.k1 + 1)) / (tf + config.k1 * (1 - config.b + config.b * (docLength / index.avgDocLength)));
83
+ score += idf * tfComponent;
84
+ }
85
+ return score;
86
+ }
87
+ /**
88
+ * Normalize BM25 scores to 0-1 range
89
+ */
90
+ export function normalizeBM25Scores(scores) {
91
+ const maxScore = Math.max(...Array.from(scores.values()), 1);
92
+ const normalized = new Map();
93
+ for (const [docId, score] of scores) {
94
+ normalized.set(docId, score / maxScore);
95
+ }
96
+ return normalized;
97
+ }
98
+ /**
99
+ * Create a hybrid search engine
100
+ */
101
+ export function createHybridSearchEngine(initialEntries = [], config = {}) {
102
+ const fullConfig = {
103
+ ...DEFAULT_HYBRID_CONFIG,
104
+ ...config,
105
+ bm25: { ...DEFAULT_HYBRID_CONFIG.bm25, ...config.bm25 },
106
+ };
107
+ let bm25Index = createBM25Index(initialEntries);
108
+ function rebuildIndex(entries) {
109
+ logger.info(`Rebuilding BM25 index with ${entries.length} documents`);
110
+ bm25Index = createBM25Index(entries);
111
+ }
112
+ function search(query, entries, options) {
113
+ const searchConfig = {
114
+ ...fullConfig,
115
+ ...options?.config,
116
+ };
117
+ // Tokenize query for BM25
118
+ const queryTokens = tokenize(query);
119
+ // Calculate BM25 scores for all entries
120
+ const bm25Scores = new Map();
121
+ for (const entry of entries) {
122
+ const score = bm25Score(bm25Index, entry.id, queryTokens, searchConfig.bm25);
123
+ if (score > 0) {
124
+ bm25Scores.set(entry.id, score);
125
+ }
126
+ }
127
+ const normalizedBM25 = normalizeBM25Scores(bm25Scores);
128
+ // Calculate KG scores based on connection count
129
+ const kgScores = new Map();
130
+ if (options?.kgConnections) {
131
+ const maxConnections = Math.max(...Array.from(options.kgConnections.values()).map(arr => arr.length), 1);
132
+ for (const entry of entries) {
133
+ const connections = options.kgConnections.get(entry.id)?.length ?? 0;
134
+ kgScores.set(entry.id, connections / maxConnections);
135
+ }
136
+ }
137
+ // Combine scores
138
+ const combinedResults = new Map();
139
+ for (const entry of entries) {
140
+ const vectorScore = options?.vectorScores?.get(entry.id) ?? 0;
141
+ const bm25Score = normalizedBM25.get(entry.id) ?? 0;
142
+ const kgScore = kgScores.get(entry.id) ?? 0;
143
+ // Weighted combination
144
+ const combinedScore = (vectorScore * searchConfig.vectorWeight) +
145
+ (bm25Score * searchConfig.bm25Weight) +
146
+ (kgScore * searchConfig.kgWeight);
147
+ if (combinedScore >= searchConfig.minScore) {
148
+ combinedResults.set(entry.id, {
149
+ entry,
150
+ score: combinedScore,
151
+ combinedScore,
152
+ vectorScore,
153
+ bm25Score,
154
+ kgScore,
155
+ reason: 'semantic',
156
+ });
157
+ }
158
+ }
159
+ // Sort by combined score
160
+ const results = Array.from(combinedResults.values())
161
+ .sort((a, b) => b.combinedScore - a.combinedScore)
162
+ .slice(0, searchConfig.maxResults);
163
+ logger.debug(`Hybrid search for "${query}": ${results.length} results`);
164
+ return results;
165
+ }
166
+ function getStats() {
167
+ return {
168
+ indexedDocs: bm25Index.totalDocs,
169
+ avgDocLength: Math.round(bm25Index.avgDocLength),
170
+ };
171
+ }
172
+ return {
173
+ search,
174
+ rebuildIndex,
175
+ getStats,
176
+ };
177
+ }
@@ -8,3 +8,4 @@ export * from './spaced-repetition.js';
8
8
  export * from './hopfield-network.js';
9
9
  export * from './adaptive-rag.js';
10
10
  export { createContextFragmentationEngine } from './context-fragmentation.js';
11
+ export { createHybridSearchEngine, createBM25Index, bm25Score, normalizeBM25Scores, DEFAULT_HYBRID_CONFIG } from './hybrid-search.js';
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const VERSION = '2.2.4';
1
+ export const VERSION = '2.2.5';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mark-improving-agent",
3
- "version": "2.2.4",
3
+ "version": "2.2.5",
4
4
  "description": "Self-evolving AI agent with permanent memory, identity continuity, and self-evolution — for AI agents that need to remember, learn, and evolve across sessions",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",