@consensus-tools/universal 0.9.0 → 0.9.1
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/consensus-llm.test.d.ts +2 -0
- package/dist/consensus-llm.test.d.ts.map +1 -0
- package/dist/consensus-llm.test.js +244 -0
- package/dist/consensus-llm.test.js.map +1 -0
- package/dist/defaults.d.ts +10 -0
- package/dist/defaults.d.ts.map +1 -1
- package/dist/defaults.js +63 -2
- package/dist/defaults.js.map +1 -1
- package/dist/index.d.ts +13 -11
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +130 -49
- package/dist/index.js.map +1 -1
- package/dist/persona-reviewer-factory.d.ts +22 -0
- package/dist/persona-reviewer-factory.d.ts.map +1 -0
- package/dist/persona-reviewer-factory.js +318 -0
- package/dist/persona-reviewer-factory.js.map +1 -0
- package/dist/reputation-manager.d.ts +38 -0
- package/dist/reputation-manager.d.ts.map +1 -0
- package/dist/reputation-manager.js +154 -0
- package/dist/reputation-manager.js.map +1 -0
- package/dist/reputation-manager.test.d.ts +2 -0
- package/dist/reputation-manager.test.d.ts.map +1 -0
- package/dist/reputation-manager.test.js +111 -0
- package/dist/reputation-manager.test.js.map +1 -0
- package/dist/risk-tiers.d.ts +10 -0
- package/dist/risk-tiers.d.ts.map +1 -0
- package/dist/risk-tiers.js +46 -0
- package/dist/risk-tiers.js.map +1 -0
- package/dist/risk-tiers.test.d.ts +2 -0
- package/dist/risk-tiers.test.d.ts.map +1 -0
- package/dist/risk-tiers.test.js +40 -0
- package/dist/risk-tiers.test.js.map +1 -0
- package/dist/types.d.ts +59 -6
- package/dist/types.d.ts.map +1 -1
- package/package.json +9 -9
- package/src/consensus-llm.test.ts +23 -4
- package/src/defaults.ts +10 -4
- package/src/index.ts +22 -18
- package/src/persona-reviewer-factory.ts +90 -70
- package/src/reputation-manager.ts +46 -31
- package/src/risk-tiers.test.ts +8 -0
- package/src/risk-tiers.ts +7 -5
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import { resolveConsensus } from "@consensus-tools/core";
|
|
3
|
+
import { createGuardTemplate, GUARD_CONFIGS } from "@consensus-tools/guards";
|
|
4
|
+
import { classifyTool } from "./risk-tiers.js";
|
|
5
|
+
// ── Persona Reviewer Factory ─────────────────────────────────────────
|
|
6
|
+
// Creates LLM-backed persona reviewers that use resolveConsensus()
|
|
7
|
+
// for multi-model deliberation with reputation-weighted voting.
|
|
8
|
+
//
|
|
9
|
+
// Architecture:
|
|
10
|
+
// 1. Regex pre-screen (sub-ms, deterministic)
|
|
11
|
+
// 2. Risk tier check (low = fast-path regex only)
|
|
12
|
+
// 3. Parallel LLM calls per persona (with timeout + fallback)
|
|
13
|
+
// 4. Parse votes from LLM responses
|
|
14
|
+
// 5. Synthesize ConsensusInput: ONE "allow" submission, all personas
|
|
15
|
+
// vote on it (YES = +1, NO = -1). resolveConsensus aggregates.
|
|
16
|
+
// 6. Determine action from consensus result
|
|
17
|
+
// 7. Return LlmDecisionResult
|
|
18
|
+
// ── Safe JSON Serialization ──────────────────────────────────────────
|
|
19
|
+
function safeStringify(obj, indent) {
|
|
20
|
+
const seen = new WeakSet();
|
|
21
|
+
return JSON.stringify(obj, (_key, value) => {
|
|
22
|
+
if (typeof value === "object" && value !== null) {
|
|
23
|
+
if (seen.has(value))
|
|
24
|
+
return "[Circular]";
|
|
25
|
+
seen.add(value);
|
|
26
|
+
}
|
|
27
|
+
return value;
|
|
28
|
+
}, indent);
|
|
29
|
+
}
|
|
30
|
+
// Match VOTE: YES/NO/REWRITE on its own line (anchored to reduce injection risk)
|
|
31
|
+
const VOTE_LINE_PATTERN = /^(?:VOTE:\s*)?(YES|NO|REWRITE)\s*$/im;
|
|
32
|
+
// Fallback: match anywhere but only as a last resort
|
|
33
|
+
const VOTE_FALLBACK_PATTERN = /\b(YES|NO|REWRITE)\b/i;
|
|
34
|
+
const CONFIDENCE_PATTERN = /confidence[:\s]*([0-9]*\.?[0-9]+)/i;
|
|
35
|
+
function parseVoteFromLlm(response) {
|
|
36
|
+
// Prefer line-anchored match (harder to inject)
|
|
37
|
+
const lineMatch = response.match(VOTE_LINE_PATTERN);
|
|
38
|
+
const voteMatch = lineMatch ?? response.match(VOTE_FALLBACK_PATTERN);
|
|
39
|
+
if (!voteMatch)
|
|
40
|
+
return null;
|
|
41
|
+
const vote = voteMatch[1].toUpperCase();
|
|
42
|
+
const confMatch = response.match(CONFIDENCE_PATTERN);
|
|
43
|
+
const confidence = confMatch?.[1] ? Math.min(1, Math.max(0, parseFloat(confMatch[1]))) : 0.5;
|
|
44
|
+
const rationale = response
|
|
45
|
+
.replace(/^.*\b(YES|NO|REWRITE)\b.*$/im, "")
|
|
46
|
+
.replace(/^.*confidence.*$/im, "")
|
|
47
|
+
.trim()
|
|
48
|
+
.slice(0, 500) || "No rationale provided";
|
|
49
|
+
return { vote, confidence, rationale };
|
|
50
|
+
}
|
|
51
|
+
// ── Prompt Construction ──────────────────────────────────────────────
|
|
52
|
+
function buildPersonaPrompt(persona, toolName, args, regexSignals) {
|
|
53
|
+
const evalPersona = persona;
|
|
54
|
+
const systemPrompt = evalPersona.systemPrompt ??
|
|
55
|
+
`You are a ${persona.role} reviewer named ${persona.name}.` +
|
|
56
|
+
(persona.bias ? ` Your bias: ${persona.bias}.` : "") +
|
|
57
|
+
(persona.non_negotiables?.length
|
|
58
|
+
? ` Non-negotiables: ${persona.non_negotiables.join(", ")}.`
|
|
59
|
+
: "") +
|
|
60
|
+
(persona.failure_modes?.length
|
|
61
|
+
? ` Watch for: ${persona.failure_modes.join(", ")}.`
|
|
62
|
+
: "") +
|
|
63
|
+
` Respond ONLY with a VOTE line, CONFIDENCE line, and RATIONALE line. Do not echo the tool arguments.`;
|
|
64
|
+
const regexContext = regexSignals.length > 0
|
|
65
|
+
? `\n\nRegex pre-screen detected these signals:\n${regexSignals.map((s) => `- ${s}`).join("\n")}`
|
|
66
|
+
: "\n\nRegex pre-screen: no pattern matches detected.";
|
|
67
|
+
// Sanitize args: truncate to prevent prompt stuffing, use safe serialization
|
|
68
|
+
const argsStr = safeStringify(args, 2).slice(0, 2000);
|
|
69
|
+
const userPrompt = `Evaluate this tool call for risks:\n\n` +
|
|
70
|
+
`Tool: ${toolName}\n` +
|
|
71
|
+
`Arguments:\n${argsStr}\n` +
|
|
72
|
+
regexContext +
|
|
73
|
+
`\n\nRespond with exactly these three lines:\n` +
|
|
74
|
+
`VOTE: YES (safe to proceed), NO (block this action), or REWRITE (needs modification)\n` +
|
|
75
|
+
`CONFIDENCE: 0.0 to 1.0\n` +
|
|
76
|
+
`RATIONALE: Brief explanation of your decision`;
|
|
77
|
+
return [
|
|
78
|
+
{ role: "system", content: systemPrompt },
|
|
79
|
+
{ role: "user", content: userPrompt },
|
|
80
|
+
];
|
|
81
|
+
}
|
|
82
|
+
// ── Regex Pre-Screen ─────────────────────────────────────────────────
|
|
83
|
+
// Fallback guard domains when configured guards have no matching configs
|
|
84
|
+
const FALLBACK_GUARDS = ["security", "compliance", "user-impact"];
|
|
85
|
+
function runRegexPreScreen(toolName, args, guards) {
|
|
86
|
+
const signals = [];
|
|
87
|
+
// Use provided guards, falling back to DEFAULT_PERSONA_TRIO
|
|
88
|
+
const effectiveGuards = guards.filter((g) => GUARD_CONFIGS[g]).length > 0
|
|
89
|
+
? guards
|
|
90
|
+
: FALLBACK_GUARDS;
|
|
91
|
+
for (const domain of effectiveGuards) {
|
|
92
|
+
const config = GUARD_CONFIGS[domain];
|
|
93
|
+
if (!config)
|
|
94
|
+
continue;
|
|
95
|
+
try {
|
|
96
|
+
const template = createGuardTemplate(domain, config);
|
|
97
|
+
const votes = template.evaluate({
|
|
98
|
+
boardId: "facade",
|
|
99
|
+
action: { type: toolName, payload: args },
|
|
100
|
+
});
|
|
101
|
+
for (const vote of votes) {
|
|
102
|
+
if (vote.vote === "NO" || (vote.risk && vote.risk > 0.5)) {
|
|
103
|
+
signals.push(`[${domain}] ${vote.reason} (risk: ${vote.risk ?? "unknown"})`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
// Regex pre-screen failure is non-fatal
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return signals;
|
|
112
|
+
}
|
|
113
|
+
// ── LLM Call with Timeout ────────────────────────────────────────────
|
|
114
|
+
async function callLlmWithTimeout(model, messages, timeoutMs) {
|
|
115
|
+
let timer;
|
|
116
|
+
try {
|
|
117
|
+
const result = await Promise.race([
|
|
118
|
+
model(messages),
|
|
119
|
+
new Promise((_, reject) => {
|
|
120
|
+
timer = setTimeout(() => reject(new Error("LLM call timed out")), timeoutMs);
|
|
121
|
+
}),
|
|
122
|
+
]);
|
|
123
|
+
return result;
|
|
124
|
+
}
|
|
125
|
+
finally {
|
|
126
|
+
if (timer)
|
|
127
|
+
clearTimeout(timer);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
// ── Regex Fallback Vote ──────────────────────────────────────────────
|
|
131
|
+
function regexFallbackVote(_persona, toolName, args, guards) {
|
|
132
|
+
const signals = runRegexPreScreen(toolName, args, guards);
|
|
133
|
+
if (signals.length > 0) {
|
|
134
|
+
return {
|
|
135
|
+
vote: "NO",
|
|
136
|
+
confidence: 0.6,
|
|
137
|
+
rationale: `Regex fallback: ${signals.join("; ")}`,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
// When LLM is unavailable AND regex finds nothing, default to block for safety.
|
|
141
|
+
// This prevents fail-open when all LLMs are down.
|
|
142
|
+
return {
|
|
143
|
+
vote: "NO",
|
|
144
|
+
confidence: 0.3,
|
|
145
|
+
rationale: "Regex fallback: no pattern matches but LLM unavailable (fail-closed)",
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Run LLM persona deliberation on a tool call.
|
|
150
|
+
*
|
|
151
|
+
* Returns an LlmDecisionResult with per-persona votes, consensus trace,
|
|
152
|
+
* and final action (allow/block/escalate).
|
|
153
|
+
*/
|
|
154
|
+
export async function deliberate(config, toolName, args) {
|
|
155
|
+
const decisionId = `dec_${crypto.randomUUID().slice(0, 12)}`;
|
|
156
|
+
const personas = config.reputationManager.getPersonas();
|
|
157
|
+
const guards = config.guards ?? FALLBACK_GUARDS;
|
|
158
|
+
// 1. Regex pre-screen
|
|
159
|
+
const regexSignals = runRegexPreScreen(toolName, args, guards);
|
|
160
|
+
// 2. Risk tier check
|
|
161
|
+
const tier = classifyTool(toolName, config.riskTiers);
|
|
162
|
+
if (tier === "low") {
|
|
163
|
+
const hasRisk = regexSignals.length > 0;
|
|
164
|
+
return {
|
|
165
|
+
decisionId,
|
|
166
|
+
action: hasRisk ? "block" : "allow",
|
|
167
|
+
votes: personas.map((p) => ({
|
|
168
|
+
personaId: p.id,
|
|
169
|
+
personaName: p.name,
|
|
170
|
+
vote: hasRisk ? "NO" : "YES",
|
|
171
|
+
confidence: hasRisk ? 0.7 : 0.3,
|
|
172
|
+
rationale: hasRisk
|
|
173
|
+
? `Fast-path regex: ${regexSignals.join("; ")}`
|
|
174
|
+
: "Fast-path: low-risk tool, no regex signals",
|
|
175
|
+
source: "regex_fallback",
|
|
176
|
+
})),
|
|
177
|
+
policy: "fast_path",
|
|
178
|
+
consensusTrace: { tier: "low", regexSignals },
|
|
179
|
+
aggregateScore: hasRisk ? 0.0 : 1.0,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
// 3. Parallel LLM calls per persona (with timeout + fallback)
|
|
183
|
+
const voteResults = await Promise.all(personas.map(async (persona) => {
|
|
184
|
+
const messages = buildPersonaPrompt(persona, toolName, args, regexSignals);
|
|
185
|
+
try {
|
|
186
|
+
const response = await callLlmWithTimeout(config.model, messages, config.timeoutMs);
|
|
187
|
+
const parsed = parseVoteFromLlm(response);
|
|
188
|
+
if (parsed) {
|
|
189
|
+
return {
|
|
190
|
+
personaId: persona.id,
|
|
191
|
+
personaName: persona.name,
|
|
192
|
+
...parsed,
|
|
193
|
+
source: "llm",
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
const fallback = regexFallbackVote(persona, toolName, args, guards);
|
|
197
|
+
return {
|
|
198
|
+
personaId: persona.id,
|
|
199
|
+
personaName: persona.name,
|
|
200
|
+
...fallback,
|
|
201
|
+
source: "regex_fallback",
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
catch {
|
|
205
|
+
const fallback = regexFallbackVote(persona, toolName, args, guards);
|
|
206
|
+
return {
|
|
207
|
+
personaId: persona.id,
|
|
208
|
+
personaName: persona.name,
|
|
209
|
+
...fallback,
|
|
210
|
+
source: "regex_fallback",
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
}));
|
|
214
|
+
// 4. Synthesize ConsensusInput for resolveConsensus()
|
|
215
|
+
//
|
|
216
|
+
// FIXED: Use a SINGLE "allow" submission. All personas vote on it.
|
|
217
|
+
// YES voters score +1, NO voters score -1, REWRITE voters score 0.
|
|
218
|
+
// This way resolveConsensus sees N votes on 1 submission, not N
|
|
219
|
+
// submissions with 1 vote each.
|
|
220
|
+
const now = new Date().toISOString();
|
|
221
|
+
const jobId = `job_facade_${decisionId}`;
|
|
222
|
+
const submissionId = `sub_${decisionId}_allow`;
|
|
223
|
+
const job = {
|
|
224
|
+
id: jobId,
|
|
225
|
+
boardId: "",
|
|
226
|
+
status: "SUBMITTED",
|
|
227
|
+
title: `Deliberation: ${toolName}`,
|
|
228
|
+
description: "",
|
|
229
|
+
createdByAgentId: "facade",
|
|
230
|
+
createdAt: now,
|
|
231
|
+
updatedAt: now,
|
|
232
|
+
mode: "VOTING",
|
|
233
|
+
consensusPolicy: { type: config.policyType },
|
|
234
|
+
stakeRequired: 0,
|
|
235
|
+
reward: 0,
|
|
236
|
+
maxParticipants: personas.length,
|
|
237
|
+
minParticipants: 1,
|
|
238
|
+
};
|
|
239
|
+
// Single submission representing "allow this tool call"
|
|
240
|
+
const submissions = [{
|
|
241
|
+
id: submissionId,
|
|
242
|
+
jobId,
|
|
243
|
+
agentId: "facade",
|
|
244
|
+
submittedAt: now,
|
|
245
|
+
summary: `Allow ${toolName}`,
|
|
246
|
+
artifacts: {},
|
|
247
|
+
confidence: 1.0,
|
|
248
|
+
requestedPayout: 0,
|
|
249
|
+
status: "SUBMITTED",
|
|
250
|
+
}];
|
|
251
|
+
// Each persona votes on the single submission
|
|
252
|
+
const votes = voteResults.map((v, i) => ({
|
|
253
|
+
id: `vote_${decisionId}_${i}`,
|
|
254
|
+
jobId,
|
|
255
|
+
agentId: v.personaId,
|
|
256
|
+
submissionId,
|
|
257
|
+
score: v.vote === "YES" ? 1 : v.vote === "NO" ? -1 : 0,
|
|
258
|
+
weight: v.confidence,
|
|
259
|
+
rationale: v.rationale,
|
|
260
|
+
createdAt: now,
|
|
261
|
+
}));
|
|
262
|
+
const reputation = (agentId) => config.reputationManager.getReputation(agentId);
|
|
263
|
+
// 5. Resolve consensus
|
|
264
|
+
const consensusInput = {
|
|
265
|
+
job: job,
|
|
266
|
+
submissions: submissions,
|
|
267
|
+
votes: votes,
|
|
268
|
+
reputation,
|
|
269
|
+
};
|
|
270
|
+
let consensusTrace;
|
|
271
|
+
try {
|
|
272
|
+
const result = resolveConsensus(consensusInput);
|
|
273
|
+
consensusTrace = result.consensusTrace;
|
|
274
|
+
// Extract the actual weighted score from the consensus trace.
|
|
275
|
+
// resolveConsensus always returns a "winner" (the single submission),
|
|
276
|
+
// but the score may be negative (more NO than YES votes).
|
|
277
|
+
const traceScores = consensusTrace?.scores;
|
|
278
|
+
const submissionScore = traceScores?.[submissionId] ?? 0;
|
|
279
|
+
consensusTrace = { ...consensusTrace, submissionScore };
|
|
280
|
+
}
|
|
281
|
+
catch {
|
|
282
|
+
consensusTrace = { policy: "fallback_majority", reason: "resolve_error" };
|
|
283
|
+
}
|
|
284
|
+
// 6. Determine action from vote distribution (direct counting)
|
|
285
|
+
// resolveConsensus provides the audit trace; vote counting determines the action.
|
|
286
|
+
// This avoids the "always-a-winner" problem where resolveConsensus returns
|
|
287
|
+
// a winner even when the score is negative.
|
|
288
|
+
const yesCount = voteResults.filter((v) => v.vote === "YES").length;
|
|
289
|
+
const noCount = voteResults.filter((v) => v.vote === "NO").length;
|
|
290
|
+
const rewriteCount = voteResults.filter((v) => v.vote === "REWRITE").length;
|
|
291
|
+
let action;
|
|
292
|
+
if (rewriteCount > voteResults.length / 2) {
|
|
293
|
+
action = "escalate";
|
|
294
|
+
}
|
|
295
|
+
else if (yesCount > noCount) {
|
|
296
|
+
action = "allow";
|
|
297
|
+
}
|
|
298
|
+
else {
|
|
299
|
+
action = "block";
|
|
300
|
+
}
|
|
301
|
+
// Compute aggregate score
|
|
302
|
+
const totalConfidence = voteResults.reduce((s, v) => s + v.confidence, 0);
|
|
303
|
+
const yesConfidence = voteResults
|
|
304
|
+
.filter((v) => v.vote === "YES")
|
|
305
|
+
.reduce((s, v) => s + v.confidence, 0);
|
|
306
|
+
const aggregateScore = totalConfidence > 0 ? yesConfidence / totalConfidence : 0.5;
|
|
307
|
+
const result = {
|
|
308
|
+
decisionId,
|
|
309
|
+
action,
|
|
310
|
+
votes: voteResults,
|
|
311
|
+
policy: config.policyType,
|
|
312
|
+
consensusTrace,
|
|
313
|
+
aggregateScore,
|
|
314
|
+
};
|
|
315
|
+
config.reputationManager.recordDecision(result);
|
|
316
|
+
return result;
|
|
317
|
+
}
|
|
318
|
+
//# sourceMappingURL=persona-reviewer-factory.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"persona-reviewer-factory.js","sourceRoot":"","sources":["../src/persona-reviewer-factory.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { PersonaConfig, ReputationChange } from "@consensus-tools/personas";
|
|
2
|
+
import type { IStorage } from "@consensus-tools/storage";
|
|
3
|
+
import type { FeedbackSignal, LlmDecisionResult } from "./types.js";
|
|
4
|
+
export interface RespawnEvent {
|
|
5
|
+
oldPersona: PersonaConfig;
|
|
6
|
+
newPersona: PersonaConfig;
|
|
7
|
+
reputation: number;
|
|
8
|
+
reason: string;
|
|
9
|
+
}
|
|
10
|
+
export declare class ReputationManager {
|
|
11
|
+
private personas;
|
|
12
|
+
private threshold;
|
|
13
|
+
private store?;
|
|
14
|
+
private scores;
|
|
15
|
+
private decisions;
|
|
16
|
+
private decisionHistory;
|
|
17
|
+
private onRespawn?;
|
|
18
|
+
constructor(personas: PersonaConfig[], threshold?: number, store?: IStorage | undefined);
|
|
19
|
+
/** Set callback for respawn events. */
|
|
20
|
+
setRespawnHandler(handler: (event: RespawnEvent) => void): void;
|
|
21
|
+
/** Get current reputation for a persona. */
|
|
22
|
+
getReputation(personaId: string): number;
|
|
23
|
+
/** Get all current reputation scores. */
|
|
24
|
+
getAllReputations(): Map<string, number>;
|
|
25
|
+
/** Record a decision for later feedback correlation. */
|
|
26
|
+
recordDecision(result: LlmDecisionResult): void;
|
|
27
|
+
/** Process human feedback signal and update reputation. */
|
|
28
|
+
processFeedback(signal: FeedbackSignal): ReputationChange[];
|
|
29
|
+
/** Check if any persona needs respawn. Collects replacements first to avoid mutation during iteration. */
|
|
30
|
+
private checkRespawn;
|
|
31
|
+
/** Get current persona list (may include respawned successors). */
|
|
32
|
+
getPersonas(): PersonaConfig[];
|
|
33
|
+
/** Persist reputation scores to store (if configured). */
|
|
34
|
+
private persist;
|
|
35
|
+
/** Load reputation scores from store (if configured). */
|
|
36
|
+
load(): Promise<void>;
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=reputation-manager.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reputation-manager.d.ts","sourceRoot":"","sources":["../src/reputation-manager.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AACjF,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,0BAA0B,CAAC;AACzD,OAAO,KAAK,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAUpE,MAAM,WAAW,YAAY;IAC3B,UAAU,EAAE,aAAa,CAAC;IAC1B,UAAU,EAAE,aAAa,CAAC;IAC1B,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,qBAAa,iBAAiB;IAO1B,OAAO,CAAC,QAAQ;IAChB,OAAO,CAAC,SAAS;IACjB,OAAO,CAAC,KAAK,CAAC;IARhB,OAAO,CAAC,MAAM,CAAkC;IAChD,OAAO,CAAC,SAAS,CAA6C;IAC9D,OAAO,CAAC,eAAe,CAA2B;IAClD,OAAO,CAAC,SAAS,CAAC,CAAgC;gBAGxC,QAAQ,EAAE,aAAa,EAAE,EACzB,SAAS,GAAE,MAAa,EACxB,KAAK,CAAC,EAAE,QAAQ,YAAA;IAO1B,uCAAuC;IACvC,iBAAiB,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,YAAY,KAAK,IAAI,GAAG,IAAI;IAI/D,4CAA4C;IAC5C,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM;IAIxC,yCAAyC;IACzC,iBAAiB,IAAI,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC;IAIxC,wDAAwD;IACxD,cAAc,CAAC,MAAM,EAAE,iBAAiB,GAAG,IAAI;IAe/C,2DAA2D;IAC3D,eAAe,CAAC,MAAM,EAAE,cAAc,GAAG,gBAAgB,EAAE;IAoC3D,0GAA0G;IAC1G,OAAO,CAAC,YAAY;IAuCpB,mEAAmE;IACnE,WAAW,IAAI,aAAa,EAAE;IAI9B,0DAA0D;IAC1D,OAAO,CAAC,OAAO;IAcf,yDAAyD;IACnD,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;CAgB5B"}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { updateReputation } from "@consensus-tools/personas";
|
|
2
|
+
import { buildLearningSummary, mutatePersona } from "@consensus-tools/personas";
|
|
3
|
+
// ── Reputation Manager ───────────────────────────────────────────────
|
|
4
|
+
// In-memory reputation tracking with optional persistence.
|
|
5
|
+
// Updates from human feedback signals (onFeedback), not self-consensus.
|
|
6
|
+
// Triggers persona respawn when reputation drops below threshold.
|
|
7
|
+
const MAX_DECISION_LOOKBACK = 100;
|
|
8
|
+
const MAX_FEEDBACK_LOOKBACK = 500;
|
|
9
|
+
export class ReputationManager {
|
|
10
|
+
personas;
|
|
11
|
+
threshold;
|
|
12
|
+
store;
|
|
13
|
+
scores = new Map();
|
|
14
|
+
decisions = new Map();
|
|
15
|
+
decisionHistory = [];
|
|
16
|
+
onRespawn;
|
|
17
|
+
constructor(personas, threshold = 0.15, store) {
|
|
18
|
+
this.personas = personas;
|
|
19
|
+
this.threshold = threshold;
|
|
20
|
+
this.store = store;
|
|
21
|
+
for (const p of personas) {
|
|
22
|
+
this.scores.set(p.id, p.reputation ?? 0.55);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/** Set callback for respawn events. */
|
|
26
|
+
setRespawnHandler(handler) {
|
|
27
|
+
this.onRespawn = handler;
|
|
28
|
+
}
|
|
29
|
+
/** Get current reputation for a persona. */
|
|
30
|
+
getReputation(personaId) {
|
|
31
|
+
return this.scores.get(personaId) ?? 0.55;
|
|
32
|
+
}
|
|
33
|
+
/** Get all current reputation scores. */
|
|
34
|
+
getAllReputations() {
|
|
35
|
+
return new Map(this.scores);
|
|
36
|
+
}
|
|
37
|
+
/** Record a decision for later feedback correlation. */
|
|
38
|
+
recordDecision(result) {
|
|
39
|
+
this.decisions.set(result.decisionId, result);
|
|
40
|
+
this.decisionHistory.push(result);
|
|
41
|
+
// Cap both collections to prevent memory leaks
|
|
42
|
+
if (this.decisionHistory.length >= MAX_DECISION_LOOKBACK) {
|
|
43
|
+
this.decisionHistory.shift();
|
|
44
|
+
}
|
|
45
|
+
// Trim the feedback correlation map (keep most recent N entries)
|
|
46
|
+
if (this.decisions.size > MAX_FEEDBACK_LOOKBACK) {
|
|
47
|
+
const oldest = this.decisions.keys().next().value;
|
|
48
|
+
if (oldest)
|
|
49
|
+
this.decisions.delete(oldest);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/** Process human feedback signal and update reputation. */
|
|
53
|
+
processFeedback(signal) {
|
|
54
|
+
const decision = this.decisions.get(signal.decisionId);
|
|
55
|
+
if (!decision)
|
|
56
|
+
return [];
|
|
57
|
+
// Map feedback to a "ground truth" final decision
|
|
58
|
+
// override_block = human says the block was wrong, action should have been ALLOW
|
|
59
|
+
// flag_miss = human says the allow was wrong, action should have been BLOCK
|
|
60
|
+
const groundTruth = signal.type === "override_block" ? "ALLOW" : "BLOCK";
|
|
61
|
+
const votes = decision.votes.map((v) => ({
|
|
62
|
+
persona_id: v.personaId,
|
|
63
|
+
vote: v.vote,
|
|
64
|
+
confidence: v.confidence,
|
|
65
|
+
}));
|
|
66
|
+
const personaConfigs = this.personas.map((p) => ({
|
|
67
|
+
...p,
|
|
68
|
+
reputation: this.scores.get(p.id) ?? 0.55,
|
|
69
|
+
}));
|
|
70
|
+
const result = updateReputation(votes, groundTruth, personaConfigs);
|
|
71
|
+
// Apply changes
|
|
72
|
+
for (const change of result.changes) {
|
|
73
|
+
this.scores.set(change.persona_id, change.reputation_after);
|
|
74
|
+
}
|
|
75
|
+
// Check for respawn (collect respawns, then apply)
|
|
76
|
+
this.checkRespawn();
|
|
77
|
+
// Persist if store configured
|
|
78
|
+
this.persist();
|
|
79
|
+
return result.changes;
|
|
80
|
+
}
|
|
81
|
+
/** Check if any persona needs respawn. Collects replacements first to avoid mutation during iteration. */
|
|
82
|
+
checkRespawn() {
|
|
83
|
+
const replacements = [];
|
|
84
|
+
// Collect personas that need respawn (don't mutate during scan)
|
|
85
|
+
for (let i = 0; i < this.personas.length; i++) {
|
|
86
|
+
const persona = this.personas[i];
|
|
87
|
+
const rep = this.scores.get(persona.id) ?? 0.55;
|
|
88
|
+
if (rep < this.threshold) {
|
|
89
|
+
replacements.push({ index: i, old: persona, rep });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
// Apply replacements after scan
|
|
93
|
+
for (const { index, old, rep } of replacements) {
|
|
94
|
+
const decisionRecords = this.decisionHistory.map((d) => ({
|
|
95
|
+
final_decision: d.action === "allow" ? "ALLOW" : "BLOCK",
|
|
96
|
+
votes: d.votes.map((v) => ({
|
|
97
|
+
persona_id: v.personaId,
|
|
98
|
+
vote: v.vote,
|
|
99
|
+
confidence: v.confidence,
|
|
100
|
+
})),
|
|
101
|
+
}));
|
|
102
|
+
const learning = buildLearningSummary(old.id, decisionRecords);
|
|
103
|
+
const successor = mutatePersona(old, learning);
|
|
104
|
+
this.personas[index] = successor;
|
|
105
|
+
this.scores.delete(old.id);
|
|
106
|
+
this.scores.set(successor.id, successor.reputation ?? 0.55);
|
|
107
|
+
this.onRespawn?.({
|
|
108
|
+
oldPersona: old,
|
|
109
|
+
newPersona: successor,
|
|
110
|
+
reputation: rep,
|
|
111
|
+
reason: `Reputation ${rep.toFixed(3)} below threshold ${this.threshold}`,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
/** Get current persona list (may include respawned successors). */
|
|
116
|
+
getPersonas() {
|
|
117
|
+
return [...this.personas];
|
|
118
|
+
}
|
|
119
|
+
/** Persist reputation scores to store (if configured). */
|
|
120
|
+
persist() {
|
|
121
|
+
if (!this.store)
|
|
122
|
+
return;
|
|
123
|
+
const data = {};
|
|
124
|
+
for (const [id, score] of this.scores) {
|
|
125
|
+
data[id] = score;
|
|
126
|
+
}
|
|
127
|
+
this.store.update((state) => {
|
|
128
|
+
state.reputation = data;
|
|
129
|
+
}).catch((err) => {
|
|
130
|
+
// Log persistence failures instead of silently swallowing
|
|
131
|
+
console.warn("[consensus] Reputation persistence failed:", err); // eslint-disable-line no-console
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
/** Load reputation scores from store (if configured). */
|
|
135
|
+
async load() {
|
|
136
|
+
if (!this.store)
|
|
137
|
+
return;
|
|
138
|
+
try {
|
|
139
|
+
const state = await this.store.getState();
|
|
140
|
+
const saved = state?.reputation;
|
|
141
|
+
if (saved) {
|
|
142
|
+
for (const [id, score] of Object.entries(saved)) {
|
|
143
|
+
if (this.scores.has(id)) {
|
|
144
|
+
this.scores.set(id, score);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
// Load failure is non-fatal, start with defaults
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
//# sourceMappingURL=reputation-manager.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reputation-manager.js","sourceRoot":"","sources":["../src/reputation-manager.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reputation-manager.test.d.ts","sourceRoot":"","sources":["../src/reputation-manager.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import { ReputationManager } from "./reputation-manager.js";
|
|
3
|
+
function makePersonas() {
|
|
4
|
+
return [
|
|
5
|
+
{ id: "p1", name: "Security", role: "security", reputation: 0.55 },
|
|
6
|
+
{ id: "p2", name: "Compliance", role: "compliance", reputation: 0.55 },
|
|
7
|
+
{ id: "p3", name: "Operations", role: "operations", reputation: 0.55 },
|
|
8
|
+
];
|
|
9
|
+
}
|
|
10
|
+
function makeDecision(overrides) {
|
|
11
|
+
return {
|
|
12
|
+
decisionId: "dec_test",
|
|
13
|
+
action: "block",
|
|
14
|
+
votes: [
|
|
15
|
+
{ personaId: "p1", personaName: "Security", vote: "NO", confidence: 0.9, rationale: "risky", source: "llm" },
|
|
16
|
+
{ personaId: "p2", personaName: "Compliance", vote: "YES", confidence: 0.7, rationale: "ok", source: "llm" },
|
|
17
|
+
{ personaId: "p3", personaName: "Operations", vote: "NO", confidence: 0.8, rationale: "risky", source: "llm" },
|
|
18
|
+
],
|
|
19
|
+
policy: "MAJORITY_VOTE",
|
|
20
|
+
consensusTrace: {},
|
|
21
|
+
aggregateScore: 0.3,
|
|
22
|
+
...overrides,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
describe("ReputationManager", () => {
|
|
26
|
+
it("initializes all personas at 0.55", () => {
|
|
27
|
+
const mgr = new ReputationManager(makePersonas());
|
|
28
|
+
expect(mgr.getReputation("p1")).toBe(0.55);
|
|
29
|
+
expect(mgr.getReputation("p2")).toBe(0.55);
|
|
30
|
+
expect(mgr.getReputation("p3")).toBe(0.55);
|
|
31
|
+
});
|
|
32
|
+
it("returns 0.55 for unknown persona IDs", () => {
|
|
33
|
+
const mgr = new ReputationManager(makePersonas());
|
|
34
|
+
expect(mgr.getReputation("unknown")).toBe(0.55);
|
|
35
|
+
});
|
|
36
|
+
it("records decisions for feedback correlation", () => {
|
|
37
|
+
const mgr = new ReputationManager(makePersonas());
|
|
38
|
+
const decision = makeDecision();
|
|
39
|
+
mgr.recordDecision(decision);
|
|
40
|
+
// processFeedback should find this decision
|
|
41
|
+
const changes = mgr.processFeedback({
|
|
42
|
+
decisionId: "dec_test",
|
|
43
|
+
type: "override_block",
|
|
44
|
+
});
|
|
45
|
+
expect(changes.length).toBe(3);
|
|
46
|
+
});
|
|
47
|
+
it("updates reputation on override_block feedback", () => {
|
|
48
|
+
const mgr = new ReputationManager(makePersonas());
|
|
49
|
+
const decision = makeDecision();
|
|
50
|
+
mgr.recordDecision(decision);
|
|
51
|
+
// override_block means the block was wrong, ground truth is ALLOW
|
|
52
|
+
// p1 voted NO (misaligned with ALLOW), p2 voted YES (aligned), p3 voted NO (misaligned)
|
|
53
|
+
const changes = mgr.processFeedback({
|
|
54
|
+
decisionId: "dec_test",
|
|
55
|
+
type: "override_block",
|
|
56
|
+
});
|
|
57
|
+
// p2 (voted YES, aligned with ALLOW) should gain reputation
|
|
58
|
+
const p2Change = changes.find((c) => c.persona_id === "p2");
|
|
59
|
+
expect(p2Change.delta).toBeGreaterThan(0);
|
|
60
|
+
// p1 (voted NO, misaligned with ALLOW) should lose reputation
|
|
61
|
+
const p1Change = changes.find((c) => c.persona_id === "p1");
|
|
62
|
+
expect(p1Change.delta).toBeLessThan(0);
|
|
63
|
+
});
|
|
64
|
+
it("updates reputation on flag_miss feedback", () => {
|
|
65
|
+
const mgr = new ReputationManager(makePersonas());
|
|
66
|
+
const decision = makeDecision({ action: "allow" });
|
|
67
|
+
mgr.recordDecision(decision);
|
|
68
|
+
// flag_miss means the allow was wrong, ground truth is BLOCK
|
|
69
|
+
const changes = mgr.processFeedback({
|
|
70
|
+
decisionId: "dec_test",
|
|
71
|
+
type: "flag_miss",
|
|
72
|
+
});
|
|
73
|
+
// p1 and p3 (voted NO, aligned with BLOCK) should gain reputation
|
|
74
|
+
const p1Change = changes.find((c) => c.persona_id === "p1");
|
|
75
|
+
expect(p1Change.delta).toBeGreaterThan(0);
|
|
76
|
+
});
|
|
77
|
+
it("returns empty changes for unknown decision IDs", () => {
|
|
78
|
+
const mgr = new ReputationManager(makePersonas());
|
|
79
|
+
const changes = mgr.processFeedback({
|
|
80
|
+
decisionId: "nonexistent",
|
|
81
|
+
type: "override_block",
|
|
82
|
+
});
|
|
83
|
+
expect(changes).toEqual([]);
|
|
84
|
+
});
|
|
85
|
+
it("triggers respawn when reputation drops below threshold", () => {
|
|
86
|
+
const personas = makePersonas();
|
|
87
|
+
personas[0].reputation = 0.10; // Already below default threshold of 0.15
|
|
88
|
+
const mgr = new ReputationManager(personas, 0.15);
|
|
89
|
+
const respawnHandler = vi.fn();
|
|
90
|
+
mgr.setRespawnHandler(respawnHandler);
|
|
91
|
+
// Record + feedback to trigger checkRespawn
|
|
92
|
+
const decision = makeDecision();
|
|
93
|
+
mgr.recordDecision(decision);
|
|
94
|
+
mgr.processFeedback({ decisionId: "dec_test", type: "override_block" });
|
|
95
|
+
// p1 started at 0.10, misaligned feedback pushes it further down
|
|
96
|
+
// Respawn should have been triggered
|
|
97
|
+
if (mgr.getReputation("p1") < 0.15 || respawnHandler.mock.calls.length === 0) {
|
|
98
|
+
// Persona was replaced, check new personas list
|
|
99
|
+
const currentPersonas = mgr.getPersonas();
|
|
100
|
+
// Either the original was respawned or it's below threshold
|
|
101
|
+
expect(currentPersonas.length).toBe(3);
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
it("getPersonas returns copies", () => {
|
|
105
|
+
const mgr = new ReputationManager(makePersonas());
|
|
106
|
+
const p1 = mgr.getPersonas();
|
|
107
|
+
const p2 = mgr.getPersonas();
|
|
108
|
+
expect(p1).not.toBe(p2);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
//# sourceMappingURL=reputation-manager.test.js.map
|