@consensus-tools/universal 0.9.0
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/LICENSE +201 -0
- package/README.md +451 -0
- package/dist/__tests__/defaults.test.d.ts +2 -0
- package/dist/__tests__/defaults.test.d.ts.map +1 -0
- package/dist/__tests__/defaults.test.js +55 -0
- package/dist/__tests__/defaults.test.js.map +1 -0
- package/dist/__tests__/fail-policy.test.d.ts +2 -0
- package/dist/__tests__/fail-policy.test.d.ts.map +1 -0
- package/dist/__tests__/fail-policy.test.js +80 -0
- package/dist/__tests__/fail-policy.test.js.map +1 -0
- package/dist/__tests__/frameworks.test.d.ts +2 -0
- package/dist/__tests__/frameworks.test.d.ts.map +1 -0
- package/dist/__tests__/frameworks.test.js +86 -0
- package/dist/__tests__/frameworks.test.js.map +1 -0
- package/dist/__tests__/logger.test.d.ts +2 -0
- package/dist/__tests__/logger.test.d.ts.map +1 -0
- package/dist/__tests__/logger.test.js +77 -0
- package/dist/__tests__/logger.test.js.map +1 -0
- package/dist/__tests__/resolve.test.d.ts +2 -0
- package/dist/__tests__/resolve.test.d.ts.map +1 -0
- package/dist/__tests__/resolve.test.js +71 -0
- package/dist/__tests__/resolve.test.js.map +1 -0
- package/dist/__tests__/wrap.test.d.ts +2 -0
- package/dist/__tests__/wrap.test.d.ts.map +1 -0
- package/dist/__tests__/wrap.test.js +90 -0
- package/dist/__tests__/wrap.test.js.map +1 -0
- package/dist/defaults.d.ts +20 -0
- package/dist/defaults.d.ts.map +1 -0
- package/dist/defaults.js +48 -0
- package/dist/defaults.js.map +1 -0
- package/dist/errors.d.ts +23 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +31 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +38 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +239 -0
- package/dist/index.js.map +1 -0
- package/dist/logger.d.ts +12 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +55 -0
- package/dist/logger.js.map +1 -0
- package/dist/resolve.d.ts +9 -0
- package/dist/resolve.d.ts.map +1 -0
- package/dist/resolve.js +25 -0
- package/dist/resolve.js.map +1 -0
- package/dist/types.d.ts +35 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +82 -0
- package/src/__tests__/defaults.test.ts +71 -0
- package/src/__tests__/fail-policy.test.ts +107 -0
- package/src/__tests__/frameworks.test.ts +106 -0
- package/src/__tests__/logger.test.ts +93 -0
- package/src/__tests__/resolve.test.ts +80 -0
- package/src/__tests__/wrap.test.ts +110 -0
- package/src/consensus-llm.test.ts +260 -0
- package/src/defaults.ts +124 -0
- package/src/errors.ts +35 -0
- package/src/index.ts +386 -0
- package/src/logger.ts +65 -0
- package/src/persona-reviewer-factory.ts +387 -0
- package/src/reputation-manager.test.ts +131 -0
- package/src/reputation-manager.ts +168 -0
- package/src/resolve.ts +30 -0
- package/src/risk-tiers.test.ts +36 -0
- package/src/risk-tiers.ts +49 -0
- package/src/types.ts +127 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
import { consensus as wrapWithConsensus } from "@consensus-tools/wrapper";
|
|
2
|
+
import type { DecisionResult, ReviewerFn, LifecycleHooks } from "@consensus-tools/wrapper";
|
|
3
|
+
import { createGuardTemplate, GUARD_CONFIGS } from "@consensus-tools/guards";
|
|
4
|
+
import { getPersonasByPack } from "@consensus-tools/personas";
|
|
5
|
+
import { MemoryStorage } from "@consensus-tools/storage";
|
|
6
|
+
import type { IStorage } from "@consensus-tools/storage";
|
|
7
|
+
import type { Wrappable, UniversalConfig, ToolExecutor, LlmDecisionResult } from "./types.js";
|
|
8
|
+
import { resolveWrappable } from "./resolve.js";
|
|
9
|
+
import {
|
|
10
|
+
DEFAULTS,
|
|
11
|
+
DEFAULT_PERSONA_TRIO,
|
|
12
|
+
DEFAULT_PACK,
|
|
13
|
+
DEFAULT_PERSONA_TIMEOUT_MS,
|
|
14
|
+
DEFAULT_RESPAWN_THRESHOLD,
|
|
15
|
+
policyToStrategy,
|
|
16
|
+
resolvePolicyType,
|
|
17
|
+
} from "./defaults.js";
|
|
18
|
+
import { createLogger } from "./logger.js";
|
|
19
|
+
import { ConsensusBlockedError, MissingDependencyError } from "./errors.js";
|
|
20
|
+
import { ReputationManager } from "./reputation-manager.js";
|
|
21
|
+
import { deliberate } from "./persona-reviewer-factory.js";
|
|
22
|
+
|
|
23
|
+
// ── Persona-as-guard templates (regex-only mode) ─────────────────────
|
|
24
|
+
|
|
25
|
+
function createDefaultReviewers(): ReviewerFn[] {
|
|
26
|
+
return DEFAULT_PERSONA_TRIO.map((domain) => {
|
|
27
|
+
const config = GUARD_CONFIGS[domain];
|
|
28
|
+
if (!config) {
|
|
29
|
+
throw new Error(`No guard config for default persona domain: ${domain}`);
|
|
30
|
+
}
|
|
31
|
+
return createGuardTemplate(domain, config).asReviewer();
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function createReviewersForGuards(guards: string[]): ReviewerFn[] {
|
|
36
|
+
return guards.map((domain) => {
|
|
37
|
+
const config = GUARD_CONFIGS[domain] ?? {
|
|
38
|
+
description: `Custom guard: ${domain}`,
|
|
39
|
+
rules: () => [{ evaluator: domain, vote: "YES" as const, reason: "No rules configured", risk: 0.1 }],
|
|
40
|
+
};
|
|
41
|
+
return createGuardTemplate(domain, config).asReviewer();
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ── Storage helpers ──────────────────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
function resolveStorage(storage: "memory" | IStorage): IStorage {
|
|
48
|
+
if (storage === "memory") {
|
|
49
|
+
return new MemoryStorage();
|
|
50
|
+
}
|
|
51
|
+
return storage;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function createStorageHooks(store: IStorage): LifecycleHooks {
|
|
55
|
+
return {
|
|
56
|
+
async afterResolve(result: DecisionResult) {
|
|
57
|
+
await store.update((state) => {
|
|
58
|
+
state.audit.push({
|
|
59
|
+
id: `audit-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
60
|
+
at: new Date().toISOString(),
|
|
61
|
+
action: result.action,
|
|
62
|
+
aggregateScore: result.aggregateScore,
|
|
63
|
+
attempt: result.attempt,
|
|
64
|
+
scoresCount: result.scores.length,
|
|
65
|
+
} as any);
|
|
66
|
+
});
|
|
67
|
+
},
|
|
68
|
+
async onBlock(result: DecisionResult) {
|
|
69
|
+
await store.update((state) => {
|
|
70
|
+
state.audit.push({
|
|
71
|
+
id: `audit-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
72
|
+
at: new Date().toISOString(),
|
|
73
|
+
action: "block",
|
|
74
|
+
aggregateScore: result.aggregateScore,
|
|
75
|
+
attempt: result.attempt,
|
|
76
|
+
scoresCount: result.scores.length,
|
|
77
|
+
} as any);
|
|
78
|
+
});
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function mergeHooks(...hookSets: LifecycleHooks[]): LifecycleHooks {
|
|
84
|
+
return {
|
|
85
|
+
async beforeSubmit(args: unknown[]) {
|
|
86
|
+
for (const h of hookSets) await h.beforeSubmit?.(args);
|
|
87
|
+
},
|
|
88
|
+
async afterResolve(result: DecisionResult) {
|
|
89
|
+
for (const h of hookSets) await h.afterResolve?.(result);
|
|
90
|
+
},
|
|
91
|
+
async onBlock(result: DecisionResult) {
|
|
92
|
+
for (const h of hookSets) await h.onBlock?.(result);
|
|
93
|
+
},
|
|
94
|
+
async onEscalate(result: DecisionResult) {
|
|
95
|
+
for (const h of hookSets) await h.onEscalate?.(result);
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ── LLM Persona Mode Setup ──────────────────────────────────────────
|
|
101
|
+
|
|
102
|
+
function createLlmExecutor(
|
|
103
|
+
fn: ToolExecutor,
|
|
104
|
+
config: Required<Pick<UniversalConfig, "policy" | "failPolicy">> & Partial<UniversalConfig>,
|
|
105
|
+
): ToolExecutor {
|
|
106
|
+
const pack = config.pack ?? DEFAULT_PACK;
|
|
107
|
+
const personas = config.personas ?? getPersonasByPack(pack);
|
|
108
|
+
const policyType = resolvePolicyType(config.policy);
|
|
109
|
+
const timeoutMs = config.personaTimeout ?? DEFAULT_PERSONA_TIMEOUT_MS;
|
|
110
|
+
const respawnThreshold = config.respawnThreshold ?? DEFAULT_RESPAWN_THRESHOLD;
|
|
111
|
+
const mode = config.mode ?? "enforce";
|
|
112
|
+
|
|
113
|
+
// Create reputation manager
|
|
114
|
+
const reputationManager = new ReputationManager(
|
|
115
|
+
personas,
|
|
116
|
+
respawnThreshold,
|
|
117
|
+
config.reputationStore,
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
// Wire respawn events to logger
|
|
121
|
+
reputationManager.setRespawnHandler((event) => {
|
|
122
|
+
if (config.logger !== false) {
|
|
123
|
+
const logFn = typeof config.logger === "function"
|
|
124
|
+
? config.logger
|
|
125
|
+
: (e: any) => console.debug("[consensus]", e.event, e.data); // eslint-disable-line no-console
|
|
126
|
+
logFn({
|
|
127
|
+
event: "persona.respawned",
|
|
128
|
+
data: {
|
|
129
|
+
oldPersonaId: event.oldPersona.id,
|
|
130
|
+
newPersonaId: event.newPersona.id,
|
|
131
|
+
reputation: event.reputation,
|
|
132
|
+
reason: event.reason,
|
|
133
|
+
},
|
|
134
|
+
timestamp: Date.now(),
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// Load persisted reputation if store is configured
|
|
140
|
+
if (config.reputationStore) {
|
|
141
|
+
reputationManager.load().catch((err) => {
|
|
142
|
+
if (config.logger !== false) {
|
|
143
|
+
console.warn("[consensus] Failed to load persisted reputation, starting with defaults:", err); // eslint-disable-line no-console
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Build the deliberation config
|
|
149
|
+
const deliberateConfig = {
|
|
150
|
+
model: config.model!,
|
|
151
|
+
pack,
|
|
152
|
+
personas: config.personas,
|
|
153
|
+
guards: config.guards ?? DEFAULTS.guards,
|
|
154
|
+
policyType,
|
|
155
|
+
riskTiers: config.riskTiers,
|
|
156
|
+
reputationManager,
|
|
157
|
+
timeoutMs,
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
return async (toolName: string, args: Record<string, unknown>): Promise<unknown> => {
|
|
161
|
+
try {
|
|
162
|
+
const decision: LlmDecisionResult = await deliberate(deliberateConfig, toolName, args);
|
|
163
|
+
|
|
164
|
+
// Fire onDecision callback
|
|
165
|
+
if (config.onDecision) {
|
|
166
|
+
await config.onDecision(decision);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Shadow mode: always allow, just log
|
|
170
|
+
if (mode === "shadow") {
|
|
171
|
+
return fn(toolName, args);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Enforce mode: act on decision
|
|
175
|
+
if (decision.action === "allow") {
|
|
176
|
+
return fn(toolName, args);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Blocked or escalated
|
|
180
|
+
if (config.failPolicy === "closed") {
|
|
181
|
+
const rationales = decision.votes.map((v) => `${v.personaName}: ${v.rationale}`).join("; ");
|
|
182
|
+
throw new ConsensusBlockedError(
|
|
183
|
+
`Consensus ${decision.action}: score ${decision.aggregateScore.toFixed(2)} ` +
|
|
184
|
+
`[${decision.policy}] (${rationales})`,
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// failPolicy: 'open' — execute anyway
|
|
189
|
+
return fn(toolName, args);
|
|
190
|
+
} catch (err) {
|
|
191
|
+
if (err instanceof ConsensusBlockedError) {
|
|
192
|
+
throw err;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Unexpected error during LLM deliberation — fall back to executing the tool
|
|
196
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
197
|
+
config.onError?.(error, { toolName, args });
|
|
198
|
+
|
|
199
|
+
if (config.failPolicy === "closed") {
|
|
200
|
+
throw new ConsensusBlockedError("LLM deliberation failed", error);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// failPolicy: 'open' — execute despite error
|
|
204
|
+
return fn(toolName, args);
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// ── Main Facade ──────────────────────────────────────────────────────
|
|
210
|
+
|
|
211
|
+
export const consensus = {
|
|
212
|
+
/**
|
|
213
|
+
* Wrap any tool executor with consensus governance.
|
|
214
|
+
*
|
|
215
|
+
* Two modes:
|
|
216
|
+
* - **Regex mode** (default): Fast, deterministic pattern-matching guards.
|
|
217
|
+
* `consensus.wrap(executor)` or `consensus.wrap(executor, { policy: "majority" })`
|
|
218
|
+
*
|
|
219
|
+
* - **LLM persona mode**: Multi-model deliberation with reputation tracking.
|
|
220
|
+
* `consensus.wrap(executor, { model: myLlm, policy: "weighted_reputation" })`
|
|
221
|
+
* Activated when `model` is provided in config.
|
|
222
|
+
*
|
|
223
|
+
* @param wrappable - A function, or object with .execute/.invoke/.call
|
|
224
|
+
* @param config - Optional configuration overrides
|
|
225
|
+
* @returns A wrapped function that runs consensus deliberation before allowing execution
|
|
226
|
+
*/
|
|
227
|
+
wrap(
|
|
228
|
+
wrappable: Wrappable,
|
|
229
|
+
config?: Partial<UniversalConfig>,
|
|
230
|
+
): ToolExecutor {
|
|
231
|
+
const fn = resolveWrappable(wrappable);
|
|
232
|
+
const merged = { ...DEFAULTS, ...config };
|
|
233
|
+
|
|
234
|
+
// Production warnings
|
|
235
|
+
const isProduction = typeof process !== "undefined" && process.env?.["NODE_ENV"] === "production";
|
|
236
|
+
if (isProduction && merged.failPolicy === "open") {
|
|
237
|
+
console.warn("[consensus] WARNING: failPolicy 'open' in production — errors will pass through unchecked"); // eslint-disable-line no-console
|
|
238
|
+
}
|
|
239
|
+
if (isProduction && merged.storage === "memory" && !config?.model) {
|
|
240
|
+
console.warn("[consensus] WARNING: storage 'memory' in production — decisions are not persisted"); // eslint-disable-line no-console
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// ── LLM Persona Mode ──────────────────────────────────────────────
|
|
244
|
+
if (config?.model) {
|
|
245
|
+
return createLlmExecutor(fn, merged);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// ── Regex-Only Mode (unchanged from v0.8.0) ───────────────────────
|
|
249
|
+
const strategy = policyToStrategy(merged.policy);
|
|
250
|
+
|
|
251
|
+
const isDefaultGuards =
|
|
252
|
+
Array.isArray(merged.guards) &&
|
|
253
|
+
merged.guards.length === DEFAULTS.guards.length &&
|
|
254
|
+
merged.guards.every((g, i) => g === DEFAULTS.guards[i]);
|
|
255
|
+
|
|
256
|
+
const reviewers: ReviewerFn[] = isDefaultGuards
|
|
257
|
+
? createDefaultReviewers()
|
|
258
|
+
: createReviewersForGuards(merged.guards);
|
|
259
|
+
|
|
260
|
+
const loggerHooks = createLogger({ logger: merged.logger });
|
|
261
|
+
const store = resolveStorage(merged.storage);
|
|
262
|
+
const storageHooks = createStorageHooks(store);
|
|
263
|
+
const hooks = mergeHooks(loggerHooks, storageHooks);
|
|
264
|
+
|
|
265
|
+
const wrapped = wrapWithConsensus<unknown>({
|
|
266
|
+
name: "universal",
|
|
267
|
+
fn: async (...args: unknown[]) => {
|
|
268
|
+
const [toolName, toolArgs] = args as [string, Record<string, unknown>];
|
|
269
|
+
return fn(toolName, toolArgs);
|
|
270
|
+
},
|
|
271
|
+
reviewers,
|
|
272
|
+
strategy,
|
|
273
|
+
hooks,
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
return async (toolName: string, args: Record<string, unknown>): Promise<unknown> => {
|
|
277
|
+
try {
|
|
278
|
+
const result: DecisionResult<unknown> = await wrapped(toolName, args);
|
|
279
|
+
|
|
280
|
+
if (merged.onDecision) {
|
|
281
|
+
await merged.onDecision(result);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (result.action === "allow") {
|
|
285
|
+
return result.output;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (merged.failPolicy === "closed") {
|
|
289
|
+
throw new ConsensusBlockedError(
|
|
290
|
+
`Consensus ${result.action}: aggregate score ${result.aggregateScore.toFixed(2)} ` +
|
|
291
|
+
`(${result.scores.map((s) => s.rationale ?? "no rationale").join("; ")})`,
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return fn(toolName, args);
|
|
296
|
+
} catch (err) {
|
|
297
|
+
if (err instanceof ConsensusBlockedError) {
|
|
298
|
+
throw err;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
302
|
+
merged.onError?.(error, { toolName, args });
|
|
303
|
+
|
|
304
|
+
if (merged.failPolicy === "closed") {
|
|
305
|
+
throw new ConsensusBlockedError("Consensus deliberation failed", error);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return fn(toolName, args);
|
|
309
|
+
}
|
|
310
|
+
};
|
|
311
|
+
},
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* LangChain adapter — dynamically loads @consensus-tools/langchain.
|
|
315
|
+
*/
|
|
316
|
+
async langchain(_chain: unknown, config?: Partial<UniversalConfig>): Promise<unknown> {
|
|
317
|
+
let mod: Record<string, unknown>;
|
|
318
|
+
try {
|
|
319
|
+
mod = await import("@consensus-tools/langchain") as Record<string, unknown>;
|
|
320
|
+
} catch {
|
|
321
|
+
throw new MissingDependencyError("@consensus-tools/langchain");
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const HandlerClass = mod["ConsensusGuardCallbackHandler"] as
|
|
325
|
+
| (new (config: Record<string, unknown>) => unknown)
|
|
326
|
+
| undefined;
|
|
327
|
+
|
|
328
|
+
if (!HandlerClass) {
|
|
329
|
+
throw new Error("@consensus-tools/langchain does not export ConsensusGuardCallbackHandler");
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const handler = new HandlerClass({
|
|
333
|
+
policy: config?.policy ?? "majority",
|
|
334
|
+
guards: config?.guards,
|
|
335
|
+
onDecision: config?.onDecision ? (d: unknown) => config.onDecision?.(d as any) : undefined,
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
return handler;
|
|
339
|
+
},
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* AI SDK (Vercel) adapter — dynamically loads @consensus-tools/ai-sdk.
|
|
343
|
+
*/
|
|
344
|
+
async aiSdk(fn: unknown, config?: Partial<UniversalConfig>): Promise<unknown> {
|
|
345
|
+
let mod: Record<string, unknown>;
|
|
346
|
+
try {
|
|
347
|
+
mod = await import("@consensus-tools/ai-sdk") as Record<string, unknown>;
|
|
348
|
+
} catch {
|
|
349
|
+
throw new MissingDependencyError("@consensus-tools/ai-sdk");
|
|
350
|
+
}
|
|
351
|
+
if (typeof mod["createGuardedGenerate"] === "function") {
|
|
352
|
+
return (mod["createGuardedGenerate"] as (fn: unknown, config?: unknown) => unknown)(fn, config);
|
|
353
|
+
}
|
|
354
|
+
throw new Error("@consensus-tools/ai-sdk does not export createGuardedGenerate");
|
|
355
|
+
},
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* MCP adapter — dynamically loads @consensus-tools/mcp.
|
|
359
|
+
*/
|
|
360
|
+
async mcp(config?: Partial<UniversalConfig>): Promise<unknown> {
|
|
361
|
+
let mod: Record<string, unknown>;
|
|
362
|
+
try {
|
|
363
|
+
mod = await import("@consensus-tools/mcp") as Record<string, unknown>;
|
|
364
|
+
} catch {
|
|
365
|
+
throw new MissingDependencyError("@consensus-tools/mcp");
|
|
366
|
+
}
|
|
367
|
+
if (typeof mod["createMcpServer"] === "function") {
|
|
368
|
+
return (mod["createMcpServer"] as (config?: unknown) => unknown)(config);
|
|
369
|
+
}
|
|
370
|
+
throw new Error("@consensus-tools/mcp does not export createMcpServer");
|
|
371
|
+
},
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
// ── Re-exports ───────────────────────────────────────────────────────
|
|
375
|
+
export { resolveWrappable } from "./resolve.js";
|
|
376
|
+
export { policyToStrategy, resolvePolicyType, DEFAULTS, DEFAULT_GUARD, DEFAULT_POLICY, DEFAULT_PERSONA_TRIO, DEFAULT_PERSONA_COUNT, DEFAULT_PACK } from "./defaults.js";
|
|
377
|
+
export { createLogger } from "./logger.js";
|
|
378
|
+
export { ConsensusBlockedError, MissingDependencyError, ConfigError } from "./errors.js";
|
|
379
|
+
export { ReputationManager } from "./reputation-manager.js";
|
|
380
|
+
export { classifyTool } from "./risk-tiers.js";
|
|
381
|
+
export { deliberate } from "./persona-reviewer-factory.js";
|
|
382
|
+
export type {
|
|
383
|
+
Wrappable, ToolExecutor, UniversalConfig, FailPolicy, ExecutionMode,
|
|
384
|
+
LogEvent, ModelAdapter, ModelMessage, LlmDecisionResult, FeedbackSignal,
|
|
385
|
+
RiskTier, RiskTierMap,
|
|
386
|
+
} from "./types.js";
|
package/src/logger.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { LifecycleHooks, DecisionResult } from "@consensus-tools/wrapper";
|
|
2
|
+
import type { LogEvent, UniversalConfig } from "./types.js";
|
|
3
|
+
|
|
4
|
+
type LogFn = (event: LogEvent) => void;
|
|
5
|
+
|
|
6
|
+
/** No-op hooks — used when logging is disabled. */
|
|
7
|
+
const NO_OP_HOOKS: LifecycleHooks = {};
|
|
8
|
+
|
|
9
|
+
function emit(logFn: LogFn, event: string, data: Record<string, unknown>): void {
|
|
10
|
+
logFn({ event, data, timestamp: Date.now() });
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Creates wrapper lifecycle hooks that emit structured log events.
|
|
15
|
+
*
|
|
16
|
+
* Events:
|
|
17
|
+
* deliberation.start — before the wrapped function runs
|
|
18
|
+
* deliberation.result — after a decision is reached (allow/block/escalate)
|
|
19
|
+
* deliberation.error — when deliberation throws
|
|
20
|
+
*/
|
|
21
|
+
export function createLogger(config: Pick<UniversalConfig, "logger">): LifecycleHooks {
|
|
22
|
+
const { logger } = config;
|
|
23
|
+
|
|
24
|
+
if (logger === false) {
|
|
25
|
+
return NO_OP_HOOKS;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const logFn: LogFn =
|
|
29
|
+
typeof logger === "function"
|
|
30
|
+
? logger
|
|
31
|
+
: (event: LogEvent) => {
|
|
32
|
+
// eslint-disable-next-line no-console
|
|
33
|
+
console.debug(`[consensus] ${event.event}`, event.data);
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
beforeSubmit(args: unknown[]) {
|
|
38
|
+
emit(logFn, "deliberation.start", { args });
|
|
39
|
+
},
|
|
40
|
+
afterResolve(result: DecisionResult) {
|
|
41
|
+
emit(logFn, "deliberation.result", {
|
|
42
|
+
action: result.action,
|
|
43
|
+
aggregateScore: result.aggregateScore,
|
|
44
|
+
attempt: result.attempt,
|
|
45
|
+
scoresCount: result.scores.length,
|
|
46
|
+
});
|
|
47
|
+
},
|
|
48
|
+
onBlock(result: DecisionResult) {
|
|
49
|
+
emit(logFn, "deliberation.result", {
|
|
50
|
+
action: "block",
|
|
51
|
+
aggregateScore: result.aggregateScore,
|
|
52
|
+
attempt: result.attempt,
|
|
53
|
+
scoresCount: result.scores.length,
|
|
54
|
+
});
|
|
55
|
+
},
|
|
56
|
+
onEscalate(result: DecisionResult) {
|
|
57
|
+
emit(logFn, "deliberation.result", {
|
|
58
|
+
action: "escalate",
|
|
59
|
+
aggregateScore: result.aggregateScore,
|
|
60
|
+
attempt: result.attempt,
|
|
61
|
+
scoresCount: result.scores.length,
|
|
62
|
+
});
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
}
|