@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
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import { consensus } from "./index.js";
|
|
3
|
+
import { ConsensusBlockedError } from "./errors.js";
|
|
4
|
+
import type { ModelAdapter, ModelMessage, LlmDecisionResult } from "./types.js";
|
|
5
|
+
|
|
6
|
+
// ── Mock Model Adapter ──────────────────────────────────────────────
|
|
7
|
+
|
|
8
|
+
function createMockModel(response: string): ModelAdapter {
|
|
9
|
+
return async (_messages: ModelMessage[]) => response;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function createAllowModel(): ModelAdapter {
|
|
13
|
+
return createMockModel("VOTE: YES\nCONFIDENCE: 0.9\nRATIONALE: Looks safe to proceed.");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function createBlockModel(): ModelAdapter {
|
|
17
|
+
return createMockModel("VOTE: NO\nCONFIDENCE: 0.95\nRATIONALE: This action is dangerous.");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function createFailingModel(): ModelAdapter {
|
|
21
|
+
return async () => { throw new Error("LLM unavailable"); };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function createSlowModel(delayMs: number): ModelAdapter {
|
|
25
|
+
return async () => {
|
|
26
|
+
await new Promise((r) => setTimeout(r, delayMs));
|
|
27
|
+
return "VOTE: YES\nCONFIDENCE: 0.8\nRATIONALE: ok";
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ── Mock Executor ────────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
function createMockExecutor() {
|
|
34
|
+
return vi.fn(async (toolName: string, args: Record<string, unknown>) => {
|
|
35
|
+
return { tool: toolName, result: "executed", args };
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ── Tests ────────────────────────────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
describe("consensus.wrap with LLM persona mode", () => {
|
|
42
|
+
describe("backward compatibility", () => {
|
|
43
|
+
it("works without model (regex-only mode)", async () => {
|
|
44
|
+
const executor = createMockExecutor();
|
|
45
|
+
const safe = consensus.wrap(executor);
|
|
46
|
+
const result = await safe("get_weather", { city: "SF" });
|
|
47
|
+
expect(executor).toHaveBeenCalledWith("get_weather", { city: "SF" });
|
|
48
|
+
expect(result).toEqual({ tool: "get_weather", result: "executed", args: { city: "SF" } });
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe("LLM persona mode - allow", () => {
|
|
53
|
+
it("allows safe tool calls when all personas vote YES", async () => {
|
|
54
|
+
const executor = createMockExecutor();
|
|
55
|
+
const safe = consensus.wrap(executor, {
|
|
56
|
+
model: createAllowModel(),
|
|
57
|
+
logger: false,
|
|
58
|
+
});
|
|
59
|
+
const result = await safe("send_email", { to: "user@test.com", body: "hello" });
|
|
60
|
+
expect(executor).toHaveBeenCalled();
|
|
61
|
+
expect(result).toBeDefined();
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe("LLM persona mode - block", () => {
|
|
66
|
+
it("blocks dangerous tool calls when personas vote NO", async () => {
|
|
67
|
+
const executor = createMockExecutor();
|
|
68
|
+
const safe = consensus.wrap(executor, {
|
|
69
|
+
model: createBlockModel(),
|
|
70
|
+
failPolicy: "closed",
|
|
71
|
+
logger: false,
|
|
72
|
+
});
|
|
73
|
+
await expect(
|
|
74
|
+
safe("delete_database", { target: "production" }),
|
|
75
|
+
).rejects.toThrow(ConsensusBlockedError);
|
|
76
|
+
expect(executor).not.toHaveBeenCalled();
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe("shadow mode", () => {
|
|
81
|
+
it("never blocks in shadow mode, even when personas vote NO", async () => {
|
|
82
|
+
const executor = createMockExecutor();
|
|
83
|
+
const safe = consensus.wrap(executor, {
|
|
84
|
+
model: createBlockModel(),
|
|
85
|
+
mode: "shadow",
|
|
86
|
+
logger: false,
|
|
87
|
+
});
|
|
88
|
+
const result = await safe("delete_database", { target: "production" });
|
|
89
|
+
expect(executor).toHaveBeenCalled();
|
|
90
|
+
expect(result).toBeDefined();
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("still fires onDecision in shadow mode", async () => {
|
|
94
|
+
const onDecision = vi.fn();
|
|
95
|
+
const executor = createMockExecutor();
|
|
96
|
+
const safe = consensus.wrap(executor, {
|
|
97
|
+
model: createBlockModel(),
|
|
98
|
+
mode: "shadow",
|
|
99
|
+
onDecision,
|
|
100
|
+
logger: false,
|
|
101
|
+
});
|
|
102
|
+
await safe("delete_database", {});
|
|
103
|
+
expect(onDecision).toHaveBeenCalled();
|
|
104
|
+
const decision = onDecision.mock.calls[0]![0] as LlmDecisionResult;
|
|
105
|
+
expect(decision.action).toBe("block");
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
describe("fast-path (low-risk tools)", () => {
|
|
110
|
+
it("skips LLM calls for low-risk tools", async () => {
|
|
111
|
+
const modelCalls: ModelMessage[][] = [];
|
|
112
|
+
const model: ModelAdapter = async (msgs) => {
|
|
113
|
+
modelCalls.push(msgs);
|
|
114
|
+
return "VOTE: YES\nCONFIDENCE: 0.9\nRATIONALE: ok";
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const executor = createMockExecutor();
|
|
118
|
+
const safe = consensus.wrap(executor, { model, logger: false });
|
|
119
|
+
await safe("get_weather", { city: "SF" });
|
|
120
|
+
|
|
121
|
+
// Low-risk tool should not trigger LLM calls
|
|
122
|
+
expect(modelCalls.length).toBe(0);
|
|
123
|
+
expect(executor).toHaveBeenCalled();
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("runs full LLM deliberation for high-risk tools", async () => {
|
|
127
|
+
const modelCalls: ModelMessage[][] = [];
|
|
128
|
+
const model: ModelAdapter = async (msgs) => {
|
|
129
|
+
modelCalls.push(msgs);
|
|
130
|
+
return "VOTE: YES\nCONFIDENCE: 0.9\nRATIONALE: ok";
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const executor = createMockExecutor();
|
|
134
|
+
const safe = consensus.wrap(executor, { model, logger: false });
|
|
135
|
+
await safe("send_email", { to: "user@test.com" });
|
|
136
|
+
|
|
137
|
+
// High-risk tool should trigger 3 LLM calls (one per persona)
|
|
138
|
+
expect(modelCalls.length).toBe(3);
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
describe("LLM failure fallback", () => {
|
|
143
|
+
it("falls back to regex when all LLM calls fail", async () => {
|
|
144
|
+
const executor = createMockExecutor();
|
|
145
|
+
const safe = consensus.wrap(executor, {
|
|
146
|
+
model: createFailingModel(),
|
|
147
|
+
failPolicy: "open",
|
|
148
|
+
logger: false,
|
|
149
|
+
});
|
|
150
|
+
// Should not throw, should fall back to regex and likely allow
|
|
151
|
+
const result = await safe("send_email", { to: "user@test.com", body: "hello" });
|
|
152
|
+
expect(result).toBeDefined();
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
describe("per-persona timeout", () => {
|
|
157
|
+
it("falls back to regex for timed-out personas", async () => {
|
|
158
|
+
const executor = createMockExecutor();
|
|
159
|
+
const safe = consensus.wrap(executor, {
|
|
160
|
+
model: createSlowModel(5000), // 5 second delay, will exceed default 3s timeout
|
|
161
|
+
personaTimeout: 100, // 100ms timeout for fast test
|
|
162
|
+
logger: false,
|
|
163
|
+
});
|
|
164
|
+
// Should not hang, should fall back to regex
|
|
165
|
+
const result = await safe("send_email", { to: "user@test.com" });
|
|
166
|
+
expect(result).toBeDefined();
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
describe("onDecision callback", () => {
|
|
171
|
+
it("receives LlmDecisionResult with vote breakdown", async () => {
|
|
172
|
+
const onDecision = vi.fn();
|
|
173
|
+
const executor = createMockExecutor();
|
|
174
|
+
const safe = consensus.wrap(executor, {
|
|
175
|
+
model: createAllowModel(),
|
|
176
|
+
onDecision,
|
|
177
|
+
logger: false,
|
|
178
|
+
});
|
|
179
|
+
await safe("send_email", { to: "test@test.com" });
|
|
180
|
+
|
|
181
|
+
expect(onDecision).toHaveBeenCalledTimes(1);
|
|
182
|
+
const decision = onDecision.mock.calls[0]![0] as LlmDecisionResult;
|
|
183
|
+
expect(decision.decisionId).toBeDefined();
|
|
184
|
+
expect(decision.action).toBe("allow");
|
|
185
|
+
expect(decision.votes.length).toBe(3);
|
|
186
|
+
expect(decision.votes[0]).toHaveProperty("personaId");
|
|
187
|
+
expect(decision.votes[0]).toHaveProperty("personaName");
|
|
188
|
+
expect(decision.votes[0]).toHaveProperty("vote");
|
|
189
|
+
expect(decision.votes[0]).toHaveProperty("confidence");
|
|
190
|
+
expect(decision.votes[0]).toHaveProperty("rationale");
|
|
191
|
+
expect(decision.votes[0]).toHaveProperty("source");
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
describe("persona pack selection", () => {
|
|
196
|
+
it("uses governance pack when configured", async () => {
|
|
197
|
+
const executor = createMockExecutor();
|
|
198
|
+
// governance pack has 5 personas, should trigger 5 LLM calls
|
|
199
|
+
const modelCalls: ModelMessage[][] = [];
|
|
200
|
+
const model: ModelAdapter = async (msgs) => {
|
|
201
|
+
modelCalls.push(msgs);
|
|
202
|
+
return "VOTE: YES\nCONFIDENCE: 0.8\nRATIONALE: ok";
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
const safe = consensus.wrap(executor, {
|
|
206
|
+
model,
|
|
207
|
+
pack: "governance",
|
|
208
|
+
logger: false,
|
|
209
|
+
});
|
|
210
|
+
await safe("send_email", { to: "test@test.com" });
|
|
211
|
+
|
|
212
|
+
expect(modelCalls.length).toBe(5); // governance pack has 5 personas
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
describe("custom risk tiers", () => {
|
|
217
|
+
it("respects user-defined risk tier overrides", async () => {
|
|
218
|
+
const modelCalls: ModelMessage[][] = [];
|
|
219
|
+
const model: ModelAdapter = async (msgs) => {
|
|
220
|
+
modelCalls.push(msgs);
|
|
221
|
+
return "VOTE: YES\nCONFIDENCE: 0.9\nRATIONALE: ok";
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
const executor = createMockExecutor();
|
|
225
|
+
const safe = consensus.wrap(executor, {
|
|
226
|
+
model,
|
|
227
|
+
riskTiers: { get_weather: "high" }, // Override: treat read-only as high-risk
|
|
228
|
+
logger: false,
|
|
229
|
+
});
|
|
230
|
+
await safe("get_weather", { city: "SF" });
|
|
231
|
+
|
|
232
|
+
// Should have triggered LLM calls because we overrode to high-risk
|
|
233
|
+
expect(modelCalls.length).toBe(3);
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
describe("consensus.wrap with LLM - policy support", () => {
|
|
239
|
+
it("accepts core policy names", async () => {
|
|
240
|
+
const executor = createMockExecutor();
|
|
241
|
+
const safe = consensus.wrap(executor, {
|
|
242
|
+
model: createAllowModel(),
|
|
243
|
+
policy: "WEIGHTED_REPUTATION",
|
|
244
|
+
logger: false,
|
|
245
|
+
});
|
|
246
|
+
const result = await safe("send_email", { to: "test@test.com" });
|
|
247
|
+
expect(result).toBeDefined();
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it("accepts friendly policy names", async () => {
|
|
251
|
+
const executor = createMockExecutor();
|
|
252
|
+
const safe = consensus.wrap(executor, {
|
|
253
|
+
model: createAllowModel(),
|
|
254
|
+
policy: "weighted_reputation",
|
|
255
|
+
logger: false,
|
|
256
|
+
});
|
|
257
|
+
const result = await safe("send_email", { to: "test@test.com" });
|
|
258
|
+
expect(result).toBeDefined();
|
|
259
|
+
});
|
|
260
|
+
});
|
package/src/defaults.ts
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import type { StrategyConfig } from "@consensus-tools/wrapper";
|
|
2
|
+
import type { UniversalConfig } from "./types.js";
|
|
3
|
+
import { ConfigError } from "./errors.js";
|
|
4
|
+
export { DEFAULT_PERSONA_TRIO } from "@consensus-tools/guards";
|
|
5
|
+
|
|
6
|
+
// ── Default Configuration ────────────────────────────────────────────
|
|
7
|
+
|
|
8
|
+
export const DEFAULT_GUARD = "agent_action";
|
|
9
|
+
export const DEFAULT_POLICY = "majority";
|
|
10
|
+
export const DEFAULT_PERSONA_COUNT = 3;
|
|
11
|
+
export const DEFAULT_PACK = "default";
|
|
12
|
+
export const DEFAULT_PERSONA_TIMEOUT_MS = 3000;
|
|
13
|
+
export const DEFAULT_RESPAWN_THRESHOLD = 0.15;
|
|
14
|
+
|
|
15
|
+
export const DEFAULTS: Required<
|
|
16
|
+
Pick<UniversalConfig, "policy" | "guards" | "failPolicy" | "storage" | "logger">
|
|
17
|
+
> = {
|
|
18
|
+
policy: DEFAULT_POLICY,
|
|
19
|
+
guards: [DEFAULT_GUARD],
|
|
20
|
+
failPolicy: "closed",
|
|
21
|
+
storage: "memory",
|
|
22
|
+
logger: true,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
// ── Core Policy Type Names ───────────────────────────────────────────
|
|
26
|
+
// All 9 policies supported by resolveConsensus() in @consensus-tools/core.
|
|
27
|
+
// These are used in LLM mode where we bypass the wrapper and call
|
|
28
|
+
// resolveConsensus() directly.
|
|
29
|
+
|
|
30
|
+
export const CORE_POLICY_TYPES = new Set([
|
|
31
|
+
"FIRST_SUBMISSION_WINS",
|
|
32
|
+
"HIGHEST_CONFIDENCE_SINGLE",
|
|
33
|
+
"APPROVAL_VOTE",
|
|
34
|
+
"OWNER_PICK",
|
|
35
|
+
"TOP_K_SPLIT",
|
|
36
|
+
"MAJORITY_VOTE",
|
|
37
|
+
"WEIGHTED_VOTE_SIMPLE",
|
|
38
|
+
"WEIGHTED_REPUTATION",
|
|
39
|
+
"TRUSTED_ARBITER",
|
|
40
|
+
]);
|
|
41
|
+
|
|
42
|
+
// ── Friendly → Core Policy Mapping ───────────────────────────────────
|
|
43
|
+
// Maps friendly names (used in facade config) to core policy type names.
|
|
44
|
+
|
|
45
|
+
const FRIENDLY_TO_CORE: Record<string, string> = {
|
|
46
|
+
majority: "MAJORITY_VOTE",
|
|
47
|
+
supermajority: "APPROVAL_VOTE",
|
|
48
|
+
unanimous: "APPROVAL_VOTE",
|
|
49
|
+
weighted_reputation: "WEIGHTED_REPUTATION",
|
|
50
|
+
first_wins: "FIRST_SUBMISSION_WINS",
|
|
51
|
+
highest_confidence: "HIGHEST_CONFIDENCE_SINGLE",
|
|
52
|
+
top_k: "TOP_K_SPLIT",
|
|
53
|
+
owner_pick: "OWNER_PICK",
|
|
54
|
+
arbiter: "TRUSTED_ARBITER",
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Resolve a policy string to a core policy type name for LLM mode.
|
|
59
|
+
* Accepts friendly names, core names, and threshold:X patterns.
|
|
60
|
+
*/
|
|
61
|
+
export function resolvePolicyType(policy: string): string {
|
|
62
|
+
// Direct core policy name
|
|
63
|
+
if (CORE_POLICY_TYPES.has(policy)) return policy;
|
|
64
|
+
|
|
65
|
+
// Friendly name mapping
|
|
66
|
+
const mapped = FRIENDLY_TO_CORE[policy];
|
|
67
|
+
if (mapped) return mapped;
|
|
68
|
+
|
|
69
|
+
// threshold:X -> APPROVAL_VOTE with custom config
|
|
70
|
+
if (policy.startsWith("threshold:")) return "APPROVAL_VOTE";
|
|
71
|
+
|
|
72
|
+
// Default to MAJORITY_VOTE
|
|
73
|
+
return "MAJORITY_VOTE";
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ── Policy-to-Strategy Mapping (regex mode) ──────────────────────────
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Maps a user-facing policy name to a wrapper StrategyConfig.
|
|
80
|
+
* Used in regex-only mode (no model provided).
|
|
81
|
+
*
|
|
82
|
+
* Supported names:
|
|
83
|
+
* 'majority' -> { strategy: 'majority' }
|
|
84
|
+
* 'supermajority' -> { strategy: 'threshold', threshold: 0.67 }
|
|
85
|
+
* 'unanimous' -> { strategy: 'unanimous' }
|
|
86
|
+
* 'threshold:X' -> { strategy: 'threshold', threshold: X }
|
|
87
|
+
*
|
|
88
|
+
* @throws ConfigError for unrecognized policy names.
|
|
89
|
+
*/
|
|
90
|
+
export function policyToStrategy(policy: string): StrategyConfig {
|
|
91
|
+
switch (policy) {
|
|
92
|
+
case "majority":
|
|
93
|
+
return { strategy: "majority" };
|
|
94
|
+
case "supermajority":
|
|
95
|
+
return { strategy: "threshold", threshold: 0.67 };
|
|
96
|
+
case "unanimous":
|
|
97
|
+
return { strategy: "unanimous" };
|
|
98
|
+
default: {
|
|
99
|
+
// Handle 'threshold:X' pattern
|
|
100
|
+
if (policy.startsWith("threshold:")) {
|
|
101
|
+
const value = Number(policy.slice("threshold:".length));
|
|
102
|
+
if (Number.isNaN(value) || value < 0 || value > 1) {
|
|
103
|
+
throw new ConfigError(
|
|
104
|
+
`Invalid threshold value in policy "${policy}". Expected a number between 0 and 1.`,
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
return { strategy: "threshold", threshold: value };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// In LLM mode, core policy names are valid. In regex mode, they're not.
|
|
111
|
+
// Let the caller decide — we just throw for truly unknown strings.
|
|
112
|
+
if (CORE_POLICY_TYPES.has(policy) || FRIENDLY_TO_CORE[policy]) {
|
|
113
|
+
// Valid LLM-mode policy used in regex mode — fall back to majority
|
|
114
|
+
return { strategy: "majority" };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
throw new ConfigError(
|
|
118
|
+
`Unknown policy "${policy}". ` +
|
|
119
|
+
`Supported: 'majority', 'supermajority', 'unanimous', 'threshold:X' (where X is 0-1).` +
|
|
120
|
+
` For LLM mode, also: ${[...CORE_POLICY_TYPES].join(", ")}.`,
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thrown when consensus blocks an action and failPolicy is 'closed'.
|
|
3
|
+
*/
|
|
4
|
+
export class ConsensusBlockedError extends Error {
|
|
5
|
+
override name = "ConsensusBlockedError";
|
|
6
|
+
|
|
7
|
+
constructor(message: string, public readonly cause?: Error) {
|
|
8
|
+
super(message);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Thrown when an optional adapter package is not installed.
|
|
14
|
+
*/
|
|
15
|
+
export class MissingDependencyError extends Error {
|
|
16
|
+
override name = "MissingDependencyError";
|
|
17
|
+
|
|
18
|
+
constructor(packageName: string) {
|
|
19
|
+
super(
|
|
20
|
+
`Package "${packageName}" is required but not installed. ` +
|
|
21
|
+
`Install it with: pnpm add ${packageName}`,
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Thrown for invalid configuration values.
|
|
28
|
+
*/
|
|
29
|
+
export class ConfigError extends Error {
|
|
30
|
+
override name = "ConfigError";
|
|
31
|
+
|
|
32
|
+
constructor(message: string) {
|
|
33
|
+
super(message);
|
|
34
|
+
}
|
|
35
|
+
}
|