alphacouncil-agent 0.5.5 → 0.8.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/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/CHANGELOG.md +120 -0
- package/data/social-handles.json +25 -0
- package/docs/persona-v2-spec.md +278 -0
- package/docs/personas.md +1 -1
- package/docs/plans/0.8.0-master-models.md +155 -0
- package/docs/roadmap.md +104 -0
- package/knowledge/masters/master_buffett/manifest.json +209 -0
- package/knowledge/masters/master_duan_yongping/manifest.json +201 -0
- package/knowledge/masters/master_marks/manifest.json +182 -0
- package/knowledge/masters/master_taleb/manifest.json +182 -0
- package/mcp/lib/constants.mjs +4 -0
- package/mcp/lib/gates.mjs +3 -1
- package/mcp/lib/markdown.mjs +189 -0
- package/mcp/lib/orchestrator.mjs +73 -7
- package/mcp/lib/packets.mjs +42 -1
- package/mcp/lib/personas-v2/ablation.mjs +148 -0
- package/mcp/lib/personas-v2/bridge.mjs +187 -0
- package/mcp/lib/personas-v2/loader.mjs +118 -0
- package/mcp/lib/personas-v2/memory.mjs +156 -0
- package/mcp/lib/personas-v2/policy.mjs +187 -0
- package/mcp/lib/personas-v2/slice.mjs +96 -0
- package/mcp/lib/rpc.mjs +56 -6
- package/mcp/lib/screen.mjs +9 -1
- package/mcp/lib/sec.mjs +62 -8
- package/mcp/lib/social.mjs +32 -1
- package/package.json +6 -1
- package/personas/masters/masters-adversarial/{short_seller.md → forensic_short.md} +20 -5
- package/schemas/persona-v2.schema.json +180 -0
package/mcp/lib/orchestrator.mjs
CHANGED
|
@@ -13,6 +13,7 @@ import { debateFromCodex, dryDebate, dryPacket, extractJson, managerFallback, me
|
|
|
13
13
|
import { mapLimit, runCodex } from "./codex.mjs";
|
|
14
14
|
import { debatePrompt, masterPrompt, selectedMasters, taskPrompt } from "./prompts.mjs";
|
|
15
15
|
import { resolveSeatWeights } from "./weights.mjs";
|
|
16
|
+
import { declinedOpinion, planMasters, reconcileOpinion } from "./personas-v2/bridge.mjs";
|
|
16
17
|
|
|
17
18
|
export function visibleRun(args) {
|
|
18
19
|
const symbol = safeSymbol(args.symbol);
|
|
@@ -80,21 +81,83 @@ export function visibleAgentSpecs(run, userPrompt = "") {
|
|
|
80
81
|
].filter(Boolean).join("\n"),
|
|
81
82
|
output_contract: isChineseLanguage(run.language) ? "只返回一个 JSON debate packet。" : `Return one JSON debate packet with reader-facing fields in ${run.language}.`,
|
|
82
83
|
}));
|
|
83
|
-
|
|
84
|
+
// The deterministic pass runs first and settles, for free, every seat whose method cannot
|
|
85
|
+
// reach this security. Spawning an agent for a lens that has already declined is how the
|
|
86
|
+
// previous design produced ten confident essays over a screen that computed nothing.
|
|
87
|
+
const plan = planMasters(run, selectedMasters(run));
|
|
88
|
+
const zh = isChineseLanguage(run.language);
|
|
89
|
+
const master_agents = plan.to_run.map(({ id, decision }) => ({
|
|
84
90
|
role: id,
|
|
91
|
+
engine: decision ? "v2_method_model" : "v1_prompt",
|
|
85
92
|
title: `AlphaCouncil Agent ${run.symbol} ${id}`,
|
|
86
93
|
prompt_template: [
|
|
87
94
|
masterPrompt(id, run),
|
|
88
95
|
"",
|
|
89
|
-
|
|
96
|
+
decision ? deterministicVerdictBlock(decision, zh) : "",
|
|
97
|
+
"",
|
|
98
|
+
zh
|
|
90
99
|
? "主线程必须先粘贴已完成的 Evidence JSON,再运行这个大师议席;大师在证据之后、辩论之前运行。"
|
|
91
100
|
: "The main thread must paste the completed Evidence JSON first. Masters run after the evidence stage and before the debate.",
|
|
92
101
|
].filter(Boolean).join("\n"),
|
|
93
|
-
output_contract:
|
|
102
|
+
output_contract: zh
|
|
94
103
|
? "只返回一个 JSON master opinion。"
|
|
95
104
|
: `Return one JSON master opinion with reader-facing fields in ${run.language}.`,
|
|
96
105
|
}));
|
|
97
|
-
|
|
106
|
+
// A declined seat is settled here rather than skipped: it is written straight into the
|
|
107
|
+
// run as an out_of_scope opinion, so the completeness gate is satisfied and no agent is
|
|
108
|
+
// ever spawned for a method that cannot look.
|
|
109
|
+
if (plan.declined.length) {
|
|
110
|
+
const byId = new Map((run.master_opinions || []).map((o) => [o.master, o]));
|
|
111
|
+
for (const { id, decision } of plan.declined) {
|
|
112
|
+
if (!byId.has(id)) byId.set(id, declinedOpinion(run, id, decision));
|
|
113
|
+
}
|
|
114
|
+
run.master_opinions = selectedMasters(run).map((id) => byId.get(id)).filter(Boolean);
|
|
115
|
+
}
|
|
116
|
+
return {
|
|
117
|
+
evidence_agents,
|
|
118
|
+
master_agents,
|
|
119
|
+
debate_agents,
|
|
120
|
+
// Recorded, not hidden: a reader must be able to tell a method that judged from a method
|
|
121
|
+
// that could not look, and neither from a seat that was never offered.
|
|
122
|
+
master_decisions: plan.decisions,
|
|
123
|
+
masters_declined: plan.declined.map(({ id, decision }) => ({
|
|
124
|
+
master: id,
|
|
125
|
+
stance: decision.stance,
|
|
126
|
+
reason: decision.reason,
|
|
127
|
+
unmet: decision.eligibility?.unmet || [],
|
|
128
|
+
})),
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* The stance is already decided. The prompt says so, in the run's language, so the model
|
|
134
|
+
* writes an explanation rather than a verdict.
|
|
135
|
+
*/
|
|
136
|
+
function deterministicVerdictBlock(decision, zh) {
|
|
137
|
+
const hits = (decision.score?.hits || []).map((h) => `${h.id}=${h.actual} (>=${h.threshold}, +${h.points})`).join("; ") || "none";
|
|
138
|
+
const misses = (decision.score?.misses || []).map((m) => `${m.id}=${m.actual}`).join("; ") || "none";
|
|
139
|
+
const uncomputable = (decision.score?.uncomputable || []).map((u) => u.id).join("; ") || "none";
|
|
140
|
+
return zh
|
|
141
|
+
? [
|
|
142
|
+
"## 已确定的判决(由确定性政策产生,你不能推翻)",
|
|
143
|
+
`- 立场:${decision.stance}(依据:${decision.reason})`,
|
|
144
|
+
`- 得分:${decision.score?.score ?? "—"}/${decision.score?.max_possible ?? "—"},覆盖率 ${Math.round((decision.score?.coverage || 0) * 100)}%`,
|
|
145
|
+
`- 命中:${hits}`,
|
|
146
|
+
`- 未命中:${misses}`,
|
|
147
|
+
`- 无法计算(既不算命中也不算未命中):${uncomputable}`,
|
|
148
|
+
"",
|
|
149
|
+
"你的任务是解释这个判决为什么成立、它最可能错在哪里、以及什么证据会推翻它。**不要给出与上面不同的 stance**;如果你认为它错了,把理由写进 disagreements。",
|
|
150
|
+
].join("\n")
|
|
151
|
+
: [
|
|
152
|
+
"## Settled verdict (produced by the deterministic policy; you cannot overturn it)",
|
|
153
|
+
`- Stance: ${decision.stance} (basis: ${decision.reason})`,
|
|
154
|
+
`- Score: ${decision.score?.score ?? "—"}/${decision.score?.max_possible ?? "—"}, coverage ${Math.round((decision.score?.coverage || 0) * 100)}%`,
|
|
155
|
+
`- Hits: ${hits}`,
|
|
156
|
+
`- Misses: ${misses}`,
|
|
157
|
+
`- Uncomputable (neither a hit nor a miss): ${uncomputable}`,
|
|
158
|
+
"",
|
|
159
|
+
"Explain why this verdict holds, where it is most likely wrong, and what evidence would overturn it. **Do not return a different stance**; if you think it is wrong, put the reason in disagreements.",
|
|
160
|
+
].join("\n");
|
|
98
161
|
}
|
|
99
162
|
|
|
100
163
|
export function recordMasterOpinion(args) {
|
|
@@ -107,20 +170,23 @@ export function recordMasterOpinion(args) {
|
|
|
107
170
|
throw invalidParams(`master ${args.master} was not selected for this run. Selected: ${allowed.join(", ") || "none"}`);
|
|
108
171
|
}
|
|
109
172
|
const dir = runPath(run.run_id);
|
|
110
|
-
const
|
|
173
|
+
const normalized = normalizeMasterOpinion(
|
|
111
174
|
{ ...(args.packet || {}), thread_id: args.thread_id },
|
|
112
175
|
args.master,
|
|
113
176
|
run,
|
|
114
177
|
rawRecordText(args.packet),
|
|
115
178
|
);
|
|
179
|
+
// A narrated stance that disagrees with the arithmetic does not get to win quietly. The
|
|
180
|
+
// deterministic verdict stands and the disagreement is preserved on the record.
|
|
181
|
+
const { opinion, overridden } = reconcileOpinion(run, args.master, normalized);
|
|
116
182
|
const byId = new Map((run.master_opinions || []).map((item) => [item.master, item]));
|
|
117
183
|
byId.set(args.master, opinion);
|
|
118
184
|
run.master_opinions = allowed.map((id) => byId.get(id)).filter(Boolean);
|
|
119
185
|
writeJson(join(dir, `${args.master}.json`), opinion);
|
|
120
186
|
saveRun(run);
|
|
121
187
|
writeJson(join(dir, "evidence.json"), run);
|
|
122
|
-
appendEvent(run, "master_opinion_recorded", { master: args.master, stance: opinion.stance });
|
|
123
|
-
return { run, opinion, recorded: run.master_opinions.length, expected: allowed.length };
|
|
188
|
+
appendEvent(run, "master_opinion_recorded", { master: args.master, stance: opinion.stance, overridden });
|
|
189
|
+
return { run, opinion, overridden, recorded: run.master_opinions.length, expected: allowed.length };
|
|
124
190
|
}
|
|
125
191
|
|
|
126
192
|
export function visibleStatusAfterPacket(run) {
|
package/mcp/lib/packets.mjs
CHANGED
|
@@ -217,6 +217,47 @@ export function managerFallback(run, userPrompt = "") {
|
|
|
217
217
|
}, "portfolio_manager", run);
|
|
218
218
|
}
|
|
219
219
|
|
|
220
|
+
/**
|
|
221
|
+
* Common ways a caller says a stance that is not one of the four we store.
|
|
222
|
+
*
|
|
223
|
+
* Mapping these is not politeness. An unmapped value used to fall through to "cautious",
|
|
224
|
+
* which is a real stance carrying real weight -- so a caller writing "avoid" got a seat
|
|
225
|
+
* that looked deliberate and voted. Ten such seats render as unanimity that no master
|
|
226
|
+
* produced.
|
|
227
|
+
*/
|
|
228
|
+
const STANCE_SYNONYMS = new Map([
|
|
229
|
+
["long", "constructive"], ["bullish", "constructive"], ["buy", "constructive"],
|
|
230
|
+
["positive", "constructive"], ["overweight", "constructive"],
|
|
231
|
+
["neutral", "cautious"], ["hold", "cautious"], ["mixed", "cautious"], ["wait", "cautious"],
|
|
232
|
+
["short", "opposed"], ["bearish", "opposed"], ["sell", "opposed"], ["avoid", "opposed"],
|
|
233
|
+
["negative", "opposed"], ["underweight", "opposed"],
|
|
234
|
+
["n/a", "out_of_scope"], ["na", "out_of_scope"], ["skip", "out_of_scope"],
|
|
235
|
+
["abstain", "out_of_scope"], ["unknown", "out_of_scope"],
|
|
236
|
+
]);
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Never silently invent a stance.
|
|
240
|
+
*
|
|
241
|
+
* Anything we cannot map becomes `out_of_scope`, which weights.mjs already treats as
|
|
242
|
+
* carrying zero weight. Guessing "cautious" for an unrecognised value manufactures a
|
|
243
|
+
* confident-looking seat out of a caller's typo; declining to score it does not.
|
|
244
|
+
*/
|
|
245
|
+
export function coerceStance(value, masterId = "") {
|
|
246
|
+
if (MASTER_STANCES.includes(value)) return value;
|
|
247
|
+
if (typeof value === "string") {
|
|
248
|
+
const mapped = STANCE_SYNONYMS.get(value.trim().toLowerCase());
|
|
249
|
+
if (mapped) return mapped;
|
|
250
|
+
}
|
|
251
|
+
if (value !== undefined && value !== null && value !== "") {
|
|
252
|
+
process.emitWarning(
|
|
253
|
+
`alphacouncil: unrecognised master stance ${JSON.stringify(value)}`
|
|
254
|
+
+ `${masterId ? ` from ${masterId}` : ""}; recorded as out_of_scope (zero weight). `
|
|
255
|
+
+ `Allowed: ${MASTER_STANCES.join(", ")}.`
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
return "out_of_scope";
|
|
259
|
+
}
|
|
260
|
+
|
|
220
261
|
/**
|
|
221
262
|
* A master's opinion. Deliberately NOT a debate packet: a master issues no rating and
|
|
222
263
|
* declares no winner. out_of_scope is a first-class stance -- "by my method this name is
|
|
@@ -229,7 +270,7 @@ export function normalizeMasterOpinion(packet, masterId, run, raw = "") {
|
|
|
229
270
|
symbol: run.symbol,
|
|
230
271
|
as_of: run.as_of,
|
|
231
272
|
verdict: typeof packet?.verdict === "string" ? packet.verdict : "",
|
|
232
|
-
stance:
|
|
273
|
+
stance: coerceStance(packet?.stance, masterId),
|
|
233
274
|
summary: typeof packet?.summary === "string" ? packet.summary : raw.slice(0, LIMITS.CLEAN_LOG_BYTES),
|
|
234
275
|
key_findings: list(packet?.key_findings),
|
|
235
276
|
disagreements: list(packet?.disagreements),
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Experiments whose job is to prove the bench decorative.
|
|
3
|
+
*
|
|
4
|
+
* A feature that cannot fail its own test is a decoration. These are the cheapest and most
|
|
5
|
+
* decisive of the battery in docs/persona-v2-spec.md:
|
|
6
|
+
*
|
|
7
|
+
* name swap -- change only the label. A real method does not notice.
|
|
8
|
+
* policy swap -- run Taleb's policy under Buffett's name. A real system follows the
|
|
9
|
+
* policy; a performance follows the name.
|
|
10
|
+
*
|
|
11
|
+
* They are deterministic because the layer under test is deterministic: no model is called,
|
|
12
|
+
* so a difference in outcome can only come from the module that was swapped.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { decide } from "./policy.mjs";
|
|
16
|
+
|
|
17
|
+
/** Fields a decision may legitimately carry that are identity rather than judgment. */
|
|
18
|
+
const IDENTITY_FIELDS = new Set(["persona_id"]);
|
|
19
|
+
|
|
20
|
+
/** The judgment, with identity stripped, so two decisions can be compared as verdicts. */
|
|
21
|
+
export function judgmentOf(decision) {
|
|
22
|
+
const copy = {};
|
|
23
|
+
for (const [key, value] of Object.entries(decision || {})) {
|
|
24
|
+
if (IDENTITY_FIELDS.has(key)) continue;
|
|
25
|
+
copy[key] = value;
|
|
26
|
+
}
|
|
27
|
+
return JSON.stringify(copy);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Rename a pack without touching a single rule.
|
|
32
|
+
*
|
|
33
|
+
* If the verdict moves, the differentiation was in the label. In the deterministic layer
|
|
34
|
+
* that is a structural guarantee rather than a hope, and that is the point of testing it
|
|
35
|
+
* here: it pins the guarantee before the narrative layer, which cannot offer one, is added.
|
|
36
|
+
*/
|
|
37
|
+
export function nameSwap(pack, newId = "master_impostor", newName = "Impostor Method Model") {
|
|
38
|
+
return { ...pack, persona_id: newId, display_name: { ...pack.display_name, en: newName } };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Keep the identity, take the other model's decision policy. */
|
|
42
|
+
export function policySwap(pack, donor) {
|
|
43
|
+
return { ...pack, decision_policy: donor.decision_policy, _policy_from: donor.persona_id };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Keep the identity and policy, take the other model's evidence. */
|
|
47
|
+
export function evidenceSwap(factsA, factsB) {
|
|
48
|
+
return [factsB, factsA];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* @returns {{stable: boolean, before: string, after: string}} stable === the verdict did
|
|
53
|
+
* not move when only the name did. `false` means the system is acting.
|
|
54
|
+
*/
|
|
55
|
+
export function runNameSwap(pack, facts) {
|
|
56
|
+
const before = decide(pack, facts);
|
|
57
|
+
const after = decide(nameSwap(pack), facts);
|
|
58
|
+
return {
|
|
59
|
+
experiment: "name_swap",
|
|
60
|
+
stable: judgmentOf(before) === judgmentOf(after),
|
|
61
|
+
before: before.stance,
|
|
62
|
+
after: after.stance,
|
|
63
|
+
persona_id_before: before.persona_id,
|
|
64
|
+
persona_id_after: after.persona_id,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* @returns {{follows_policy: boolean}} true when the verdict tracked the donor policy
|
|
70
|
+
* rather than the host name.
|
|
71
|
+
*/
|
|
72
|
+
export function runPolicySwap(host, donor, facts) {
|
|
73
|
+
const hostOwn = decide(host, facts);
|
|
74
|
+
const donorOwn = decide(donor, facts);
|
|
75
|
+
const hybrid = decide(policySwap(host, donor), facts);
|
|
76
|
+
return {
|
|
77
|
+
experiment: "policy_swap",
|
|
78
|
+
host: host.persona_id,
|
|
79
|
+
donor: donor.persona_id,
|
|
80
|
+
host_stance: hostOwn.stance,
|
|
81
|
+
donor_stance: donorOwn.stance,
|
|
82
|
+
hybrid_stance: hybrid.stance,
|
|
83
|
+
follows_policy: judgmentOf(hybrid) === judgmentOf(donorOwn),
|
|
84
|
+
follows_name: judgmentOf(hybrid) === judgmentOf(hostOwn) && judgmentOf(donorOwn) !== judgmentOf(hostOwn),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Do these models actually see different securities?
|
|
90
|
+
*
|
|
91
|
+
* Pairwise agreement across a case set. Compared against a model's agreement with itself,
|
|
92
|
+
* this is what separates a bench from a chorus: if two models agree as often as one model
|
|
93
|
+
* agrees with itself, the personas produced no differentiation and the report has to say so
|
|
94
|
+
* rather than presenting them as independent seats.
|
|
95
|
+
*/
|
|
96
|
+
export function pairwiseAgreement(packs, cases) {
|
|
97
|
+
const rows = [];
|
|
98
|
+
for (let i = 0; i < packs.length; i += 1) {
|
|
99
|
+
for (let j = i + 1; j < packs.length; j += 1) {
|
|
100
|
+
let same = 0;
|
|
101
|
+
for (const facts of cases) {
|
|
102
|
+
if (decide(packs[i], facts).stance === decide(packs[j], facts).stance) same += 1;
|
|
103
|
+
}
|
|
104
|
+
rows.push({ a: packs[i].persona_id, b: packs[j].persona_id, agreement: cases.length ? same / cases.length : 0 });
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
const mean = rows.length ? rows.reduce((sum, r) => sum + r.agreement, 0) / rows.length : 0;
|
|
108
|
+
return { pairs: rows, mean_agreement: mean };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Self-consistency of the deterministic layer is 1 by construction. It is computed rather
|
|
113
|
+
* than asserted so the same diagnostic keeps working once a narrative layer -- which has no
|
|
114
|
+
* such guarantee -- sits on top of it.
|
|
115
|
+
*/
|
|
116
|
+
export function selfConsistency(pack, cases, repeats = 3) {
|
|
117
|
+
let agree = 0;
|
|
118
|
+
let total = 0;
|
|
119
|
+
for (const facts of cases) {
|
|
120
|
+
const first = decide(pack, facts).stance;
|
|
121
|
+
for (let r = 1; r < repeats; r += 1) {
|
|
122
|
+
total += 1;
|
|
123
|
+
if (decide(pack, facts).stance === first) agree += 1;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return total ? agree / total : 1;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* The verdict the report must print.
|
|
131
|
+
*
|
|
132
|
+
* `none` means the seats cannot be told apart and must not be presented as a bench. The
|
|
133
|
+
* threshold is a margin, not a hope: differentiation counts only when models disagree with
|
|
134
|
+
* each other appreciably more than a model disagrees with itself.
|
|
135
|
+
*/
|
|
136
|
+
export function differentiation(packs, cases, { margin = 0.15 } = {}) {
|
|
137
|
+
const pairwise = pairwiseAgreement(packs, cases);
|
|
138
|
+
const self = packs.length ? packs.reduce((sum, p) => sum + selfConsistency(p, cases), 0) / packs.length : 1;
|
|
139
|
+
const gap = self - pairwise.mean_agreement;
|
|
140
|
+
const verdict = gap >= margin * 2 ? "effective" : gap >= margin ? "weak" : "none";
|
|
141
|
+
return {
|
|
142
|
+
self_consistency: self,
|
|
143
|
+
mean_pairwise_agreement: pairwise.mean_agreement,
|
|
144
|
+
gap,
|
|
145
|
+
verdict,
|
|
146
|
+
pairs: pairwise.pairs,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where Persona v2 meets a live run.
|
|
3
|
+
*
|
|
4
|
+
* The grounding block a run already assembles carries almost everything a deterministic
|
|
5
|
+
* gate needs; it is simply shaped for prompts rather than for predicates. This maps it onto
|
|
6
|
+
* the fact paths the packs address, and nothing here invents a value: a field the grounding
|
|
7
|
+
* does not carry stays absent, so the gate declines rather than guesses.
|
|
8
|
+
*
|
|
9
|
+
* A pack with no v2 manifest is not an error. Masters that have not been migrated keep
|
|
10
|
+
* running as v1 prompt personas, and the run records which of the two produced each seat so
|
|
11
|
+
* a reader is never left to assume.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { loadPacks } from "./loader.mjs";
|
|
15
|
+
import { decide } from "./policy.mjs";
|
|
16
|
+
import { sliceFor } from "./slice.mjs";
|
|
17
|
+
|
|
18
|
+
let cached = null;
|
|
19
|
+
export function packs() {
|
|
20
|
+
if (!cached) cached = loadPacks();
|
|
21
|
+
return cached;
|
|
22
|
+
}
|
|
23
|
+
export function resetPacks() { cached = null; }
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Grounding -> the fact shape packs address.
|
|
27
|
+
*
|
|
28
|
+
* `screen.rules_computed` is the load-bearing one: a 20-F filer yields zero computable
|
|
29
|
+
* rules, which is exactly the condition a financial-series method must decline on.
|
|
30
|
+
*/
|
|
31
|
+
export function factsFromRun(run) {
|
|
32
|
+
const g = run?.grounding || {};
|
|
33
|
+
const screen = g.screen || {};
|
|
34
|
+
const metrics = {};
|
|
35
|
+
for (const metric of screen.metrics || []) {
|
|
36
|
+
if (metric && typeof metric.rule === "string" && metric.value !== undefined) metrics[metric.rule] = metric.value;
|
|
37
|
+
}
|
|
38
|
+
const options = g.options || {};
|
|
39
|
+
return {
|
|
40
|
+
symbol: run?.symbol,
|
|
41
|
+
as_of: run?.as_of,
|
|
42
|
+
quote: g.quote || {},
|
|
43
|
+
filer: {
|
|
44
|
+
...(g.filer || {}),
|
|
45
|
+
// The screen only computes anything when SEC structured financials cover the filer.
|
|
46
|
+
structured_financials: Number(screen.rules_computed || 0) > 0,
|
|
47
|
+
},
|
|
48
|
+
screen: {
|
|
49
|
+
rules_computed: Number(screen.rules_computed || 0),
|
|
50
|
+
rules_total: Number(screen.rules_total || 0),
|
|
51
|
+
metrics,
|
|
52
|
+
},
|
|
53
|
+
options: {
|
|
54
|
+
chain_available: Boolean(options.atm_iv || options.term_structure || options.expiries),
|
|
55
|
+
atm_iv: options.atm_iv,
|
|
56
|
+
// Mapped only where the snapshot genuinely carries them. Realized volatility, and so
|
|
57
|
+
// the implied-versus-realized gap and any friction-adjusted edge, need a history the
|
|
58
|
+
// chain does not have -- which is why IV percentile is uncomputable too. Those paths
|
|
59
|
+
// stay absent, the rules that read them score as uncomputable, and a volatility method
|
|
60
|
+
// handed a bare snapshot correctly finds too little of itself to run.
|
|
61
|
+
skew_25d_points: options.skew_25d_put_minus_call_points ?? options.skew_25d_points,
|
|
62
|
+
expiry_covers_next_event: options.expiry_covers_next_event,
|
|
63
|
+
realized_minus_implied_vol_points: options.realized_minus_implied_vol_points,
|
|
64
|
+
net_edge_vol_points: options.net_edge_vol_points,
|
|
65
|
+
},
|
|
66
|
+
macro: g.macro || {},
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* The deterministic pass over every selected master, before a single agent is spawned.
|
|
72
|
+
*
|
|
73
|
+
* Seats that cannot look are settled here for free. `to_run` is what still needs a model;
|
|
74
|
+
* `declined` is what does not, and both are recorded so the report can show the difference
|
|
75
|
+
* between a method that judged and a method that could not.
|
|
76
|
+
*/
|
|
77
|
+
export function planMasters(run, masterIds) {
|
|
78
|
+
const reg = packs();
|
|
79
|
+
const facts = factsFromRun(run);
|
|
80
|
+
const decisions = [];
|
|
81
|
+
const to_run = [];
|
|
82
|
+
const declined = [];
|
|
83
|
+
// No grounding is not the same as a screen that computed nothing. A run that was never
|
|
84
|
+
// grounded has not been measured, so the gate has no facts to judge on and must not read
|
|
85
|
+
// their absence as a failing result -- doing so would silence the whole bench on every
|
|
86
|
+
// run that skips grounding.
|
|
87
|
+
const grounded = Boolean(run?.grounding && typeof run.grounding === "object");
|
|
88
|
+
for (const id of masterIds || []) {
|
|
89
|
+
const pack = reg.get(id);
|
|
90
|
+
if (!pack || !grounded) { to_run.push({ id, engine: "v1_prompt" }); continue; }
|
|
91
|
+
const decision = decide(pack, facts);
|
|
92
|
+
decisions.push({ ...decision, persona_id: id, engine: "v2_method_model", kind: pack.kind });
|
|
93
|
+
if (decision.narratable) to_run.push({ id, engine: "v2_method_model", decision });
|
|
94
|
+
else declined.push({ id, engine: "v2_method_model", decision });
|
|
95
|
+
}
|
|
96
|
+
return { facts, decisions, to_run, declined };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* A declined seat still has to report, because the completeness gate counts every selected
|
|
101
|
+
* master and a seat that is merely skipped would leave the run permanently incomplete.
|
|
102
|
+
*
|
|
103
|
+
* The deterministic pass already produced the verdict, so the opinion is written directly
|
|
104
|
+
* and no agent is spawned. That is the whole saving: the seat is accounted for, the reader
|
|
105
|
+
* sees why it could not look, and nothing was spent to find out.
|
|
106
|
+
*/
|
|
107
|
+
export function declinedOpinion(run, id, decision) {
|
|
108
|
+
const unmet = (decision.eligibility?.unmet || [])
|
|
109
|
+
.map((u) => `${u.requirement} [${u.reason}${u.actual !== undefined ? `: ${u.actual}` : ""}]`)
|
|
110
|
+
.join("; ");
|
|
111
|
+
const pack = packs().get(id);
|
|
112
|
+
const name = pack?.display_name?.en || id;
|
|
113
|
+
return {
|
|
114
|
+
master: id,
|
|
115
|
+
symbol: run.symbol,
|
|
116
|
+
as_of: run.as_of,
|
|
117
|
+
stance: "out_of_scope",
|
|
118
|
+
verdict: `${name} cannot evaluate ${run.symbol}: ${decision.reason}`,
|
|
119
|
+
summary: decision.reason === "eligibility"
|
|
120
|
+
? `This method's entry requirements are not met for ${run.symbol}, so it returns no judgment. Unmet: ${unmet || "n/a"}. Declining is a conclusion, not an abstention, and it was reached deterministically without spending a model call.`
|
|
121
|
+
: `Too little of this method could be computed for ${run.symbol} to be judged rather than sampled (coverage ${Math.round((decision.score?.coverage || 0) * 100)}%), so it returns no judgment.`,
|
|
122
|
+
key_findings: [],
|
|
123
|
+
disagreements: [],
|
|
124
|
+
disqualifiers_triggered: (decision.eligibility?.unmet || []).map((u) => u.requirement),
|
|
125
|
+
what_would_change_my_mind: (decision.eligibility?.unmet || []).map((u) => `${u.requirement} becomes available and is met`),
|
|
126
|
+
source_ids: [],
|
|
127
|
+
confidence: "high",
|
|
128
|
+
engine: "v2_method_model",
|
|
129
|
+
deterministic_stance: "out_of_scope",
|
|
130
|
+
decision_reason: decision.reason,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* What one v2 seat is allowed to read, and the deterministic verdict it must narrate.
|
|
136
|
+
*
|
|
137
|
+
* Handed to the agent so the prompt describes a decision it cannot overturn: the model
|
|
138
|
+
* explains, it does not choose.
|
|
139
|
+
*/
|
|
140
|
+
export function briefFor(run, masterId, evidencePackets) {
|
|
141
|
+
const pack = packs().get(masterId);
|
|
142
|
+
if (!pack) return null;
|
|
143
|
+
const facts = factsFromRun(run);
|
|
144
|
+
const decision = decide(pack, facts);
|
|
145
|
+
const slice = sliceFor(pack, { frozen: facts, packets: evidencePackets || run?.packets || [] });
|
|
146
|
+
return {
|
|
147
|
+
persona_id: masterId,
|
|
148
|
+
engine: "v2_method_model",
|
|
149
|
+
kind: pack.kind,
|
|
150
|
+
display_name: pack.display_name,
|
|
151
|
+
decision,
|
|
152
|
+
visible_tasks: slice.packets.map((p) => p.task),
|
|
153
|
+
excluded_tasks: slice.excluded,
|
|
154
|
+
admission_shortfall: pack.admission_shortfall,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* A recorded opinion must not contradict the arithmetic that produced it.
|
|
160
|
+
*
|
|
161
|
+
* The model narrates; if its stance disagrees with the deterministic verdict, the
|
|
162
|
+
* deterministic one stands and the disagreement is recorded rather than resolved silently.
|
|
163
|
+
* This is the same rule that failed once already: a stance that survives normalization
|
|
164
|
+
* without being checked is how ten seats came to read `cautious`.
|
|
165
|
+
*/
|
|
166
|
+
export function reconcileOpinion(run, masterId, opinion) {
|
|
167
|
+
const pack = packs().get(masterId);
|
|
168
|
+
// Same rule as the planner: an ungrounded run has no arithmetic to reconcile against, so
|
|
169
|
+
// the narrated stance is all there is and overriding it would be inventing a verdict.
|
|
170
|
+
if (!pack || !run?.grounding) return { opinion, engine: "v1_prompt", overridden: false };
|
|
171
|
+
const decision = decide(pack, factsFromRun(run));
|
|
172
|
+
if (opinion?.stance === decision.stance) {
|
|
173
|
+
return { opinion: { ...opinion, engine: "v2_method_model", deterministic_stance: decision.stance }, engine: "v2_method_model", overridden: false };
|
|
174
|
+
}
|
|
175
|
+
return {
|
|
176
|
+
engine: "v2_method_model",
|
|
177
|
+
overridden: true,
|
|
178
|
+
opinion: {
|
|
179
|
+
...opinion,
|
|
180
|
+
stance: decision.stance,
|
|
181
|
+
engine: "v2_method_model",
|
|
182
|
+
deterministic_stance: decision.stance,
|
|
183
|
+
narrated_stance: opinion?.stance,
|
|
184
|
+
override_reason: `the deterministic ${pack.persona_id} policy returned ${decision.stance} (${decision.reason}); the narrated stance did not agree and does not govern`,
|
|
185
|
+
},
|
|
186
|
+
};
|
|
187
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Loads Persona v2 packs and enforces the admission bar in code.
|
|
3
|
+
*
|
|
4
|
+
* The bar is not a badge an author awards themselves. A pack that declares
|
|
5
|
+
* `kind: "method_model"` on a thin corpus is downgraded to `operator_lens` here, and the
|
|
6
|
+
* shortfall is carried on the pack so the report can say which one the reader is looking
|
|
7
|
+
* at. Letting a manifest self-certify would reproduce, one layer up, the exact failure this
|
|
8
|
+
* release exists to remove: a shape that looks like a judgment, holding none.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { readFileSync, readdirSync, existsSync, statSync } from "node:fs";
|
|
12
|
+
import { fileURLToPath } from "node:url";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
|
|
15
|
+
/** Minimum corpus for a pack to carry a person's name. See docs/persona-v2-spec.md. */
|
|
16
|
+
export const ADMISSION_BAR = Object.freeze({
|
|
17
|
+
propositions: 25,
|
|
18
|
+
primary_sources: 5,
|
|
19
|
+
decision_cases: 5,
|
|
20
|
+
failure_cases: 3,
|
|
21
|
+
vetoes: 10,
|
|
22
|
+
counterfactuals: 10,
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
export function defaultKnowledgeDir() {
|
|
26
|
+
return process.env.ALPHACOUNCIL_KNOWLEDGE_DIR
|
|
27
|
+
|| fileURLToPath(new URL("../../../knowledge/masters/", import.meta.url));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Counted from the corpus, never from the manifest's own claim about itself. */
|
|
31
|
+
export function countAdmission(pack) {
|
|
32
|
+
const sources = pack.sources || [];
|
|
33
|
+
return {
|
|
34
|
+
propositions: (pack.doctrine || []).length,
|
|
35
|
+
primary_sources: sources.filter((s) => s.grade === "A" || s.grade === "B").length,
|
|
36
|
+
decision_cases: (pack.decision_cases || []).length,
|
|
37
|
+
failure_cases: (pack.failure_cases || []).length
|
|
38
|
+
|| (pack.doctrine || []).filter((d) => (d.counterexamples || []).length).length,
|
|
39
|
+
vetoes: (pack.decision_policy?.vetoes || []).length,
|
|
40
|
+
counterfactuals: (pack.counterfactuals || []).length,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function admissionShortfall(counts) {
|
|
45
|
+
const short = {};
|
|
46
|
+
for (const [key, required] of Object.entries(ADMISSION_BAR)) {
|
|
47
|
+
const have = counts[key] || 0;
|
|
48
|
+
if (have < required) short[key] = { have, required };
|
|
49
|
+
}
|
|
50
|
+
return short;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* A rule may only be defined by a grade A or B source. C supports, D stages, E is rejected.
|
|
55
|
+
* A doctrine entry citing nothing loadable is dropped rather than quietly kept.
|
|
56
|
+
*/
|
|
57
|
+
export function validatePack(pack, file = "") {
|
|
58
|
+
const errors = [];
|
|
59
|
+
const fail = (m) => errors.push(`${file}: ${m}`);
|
|
60
|
+
if (pack?.schema_version !== 2) fail(`schema_version must be 2, got ${JSON.stringify(pack?.schema_version)}`);
|
|
61
|
+
if (!/^[a-z0-9_]{2,48}$/.test(pack?.persona_id || "")) fail(`persona_id invalid: ${JSON.stringify(pack?.persona_id)}`);
|
|
62
|
+
if (!pack?.display_name?.en) fail("display_name.en is required");
|
|
63
|
+
if (pack?.display_name?.en && !/model|lens/i.test(pack.display_name.en)) {
|
|
64
|
+
// Naming discipline is a correctness property, not decoration: the product claim is a
|
|
65
|
+
// method, and a display name that reads as a person overstates it on every surface.
|
|
66
|
+
fail(`display_name.en must read as a method ("Buffett Method Model"), got ${JSON.stringify(pack.display_name.en)}`);
|
|
67
|
+
}
|
|
68
|
+
if (!pack?.decision_policy?.eligibility?.requires?.length) fail("decision_policy.eligibility.requires must be non-empty");
|
|
69
|
+
if (!pack?.decision_policy?.stance_bands?.length) fail("decision_policy.stance_bands must be non-empty");
|
|
70
|
+
|
|
71
|
+
const byId = new Map((pack?.sources || []).map((s) => [s.id, s]));
|
|
72
|
+
for (const rule of pack?.doctrine || []) {
|
|
73
|
+
if (!rule.source_ids?.length) { fail(`doctrine ${rule.rule_id}: no source_ids`); continue; }
|
|
74
|
+
const defining = rule.source_ids.filter((id) => ["A", "B"].includes(byId.get(id)?.grade));
|
|
75
|
+
if (!defining.length) fail(`doctrine ${rule.rule_id}: cites no grade A/B source, so it cannot define a rule`);
|
|
76
|
+
for (const id of rule.source_ids) if (!byId.has(id)) fail(`doctrine ${rule.rule_id}: unknown source ${id}`);
|
|
77
|
+
}
|
|
78
|
+
for (const rule of pack?.decision_policy?.scoring?.rules || []) {
|
|
79
|
+
if (!rule.provenance || rule.provenance.length < 8) {
|
|
80
|
+
fail(`scoring rule ${rule.id}: threshold has no provenance; determinism moves the uncertainty into the constant`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
const leak = pack?.memory_policy?.leak_rule;
|
|
84
|
+
if (leak && leak !== "public_at <= as_of AND memory_created_at <= as_of") {
|
|
85
|
+
fail("memory_policy.leak_rule must carry both clauses or a model reads the future through its own diary");
|
|
86
|
+
}
|
|
87
|
+
return errors;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Validates, counts, and downgrades kind when the corpus does not support the name. */
|
|
91
|
+
export function loadPack(file) {
|
|
92
|
+
const pack = JSON.parse(readFileSync(file, "utf8"));
|
|
93
|
+
const errors = validatePack(pack, file);
|
|
94
|
+
if (errors.length) throw new Error(errors.join("\n"));
|
|
95
|
+
const counts = countAdmission(pack);
|
|
96
|
+
const shortfall = admissionShortfall(counts);
|
|
97
|
+
const qualifies = Object.keys(shortfall).length === 0;
|
|
98
|
+
return {
|
|
99
|
+
...pack,
|
|
100
|
+
admission_counted: counts,
|
|
101
|
+
admission_shortfall: shortfall,
|
|
102
|
+
// The manifest's own `kind` is advisory. This one is enforced.
|
|
103
|
+
kind: qualifies ? "method_model" : "operator_lens",
|
|
104
|
+
kind_declared: pack.kind,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function loadPacks({ dir = defaultKnowledgeDir() } = {}) {
|
|
109
|
+
if (!existsSync(dir)) return { packs: [], get: () => undefined };
|
|
110
|
+
const packs = [];
|
|
111
|
+
for (const entry of readdirSync(dir)) {
|
|
112
|
+
const manifest = join(dir, entry, "manifest.json");
|
|
113
|
+
if (statSync(join(dir, entry)).isDirectory() && existsSync(manifest)) packs.push(loadPack(manifest));
|
|
114
|
+
}
|
|
115
|
+
packs.sort((a, b) => a.persona_id.localeCompare(b.persona_id));
|
|
116
|
+
const index = new Map(packs.map((p) => [p.persona_id, p]));
|
|
117
|
+
return { packs, get: (id) => index.get(id) };
|
|
118
|
+
}
|