@antonbabenko/deliberation-mcp 3.3.0 → 3.5.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.
Files changed (2) hide show
  1. package/dist/index.js +292 -1
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -750,6 +750,243 @@ var require_prompts = __commonJS({
750
750
  }
751
751
  });
752
752
 
753
+ // ../../core/analyze.js
754
+ var require_analyze = __commonJS({
755
+ "../../core/analyze.js"(exports2, module2) {
756
+ "use strict";
757
+ var SLOW_FACTOR = 2;
758
+ var MIN_BASELINE_MS = 200;
759
+ var MIN_CALLS = 2;
760
+ var ABS_SLOW_MS = 12e4;
761
+ var HIGH_ERROR_RATE = 0.5;
762
+ var HIGH_AGREEMENT = 0.9;
763
+ var MIN_VOTES = 3;
764
+ var OR_PREFIX = "openrouter:";
765
+ function parseDebugLog(text) {
766
+ if (typeof text !== "string" || text.length === 0) return [];
767
+ const out = [];
768
+ for (const line of text.split("\n")) {
769
+ const trimmed = line.trim();
770
+ if (!trimmed) continue;
771
+ let obj;
772
+ try {
773
+ obj = JSON.parse(trimmed);
774
+ } catch {
775
+ continue;
776
+ }
777
+ if (obj && typeof obj === "object" && typeof obj.event === "string") {
778
+ out.push(
779
+ /** @type {DebugEvent} */
780
+ obj
781
+ );
782
+ }
783
+ }
784
+ return out;
785
+ }
786
+ function percentile(sorted, p) {
787
+ if (!sorted.length) return 0;
788
+ if (sorted.length === 1) return sorted[0];
789
+ const idx = p / 100 * (sorted.length - 1);
790
+ const lo = Math.floor(idx);
791
+ const hi = Math.ceil(idx);
792
+ if (lo === hi) return sorted[lo];
793
+ return sorted[lo] + (sorted[hi] - sorted[lo]) * (idx - lo);
794
+ }
795
+ function aggregateByModel(events) {
796
+ const groups = /* @__PURE__ */ new Map();
797
+ for (const e of Array.isArray(events) ? events : []) {
798
+ if (!e || e.event !== "provider_result" || typeof e.provider !== "string") continue;
799
+ const provider = e.provider;
800
+ const model = typeof e.model === "string" ? e.model : "";
801
+ const key = `${provider}|${model}`;
802
+ let g = groups.get(key);
803
+ if (!g) {
804
+ g = { provider, model, ms: [], errors: 0, calls: 0, tokens: [], efforts: /* @__PURE__ */ new Set(), tools: /* @__PURE__ */ new Set() };
805
+ groups.set(key, g);
806
+ }
807
+ g.calls += 1;
808
+ if (e.isError) g.errors += 1;
809
+ if (typeof e.ms === "number" && Number.isFinite(e.ms)) g.ms.push(e.ms);
810
+ const tot = e.usage && typeof e.usage.totalTokens === "number" ? e.usage.totalTokens : void 0;
811
+ if (typeof tot === "number" && Number.isFinite(tot)) g.tokens.push(tot);
812
+ g.efforts.add(e.reasoningEffort == null ? "n/a" : String(e.reasoningEffort));
813
+ if (typeof e.tool === "string") g.tools.add(e.tool);
814
+ }
815
+ const stats = [];
816
+ for (const g of groups.values()) {
817
+ const sorted = g.ms.slice().sort((a, b) => a - b);
818
+ const mean = sorted.length ? sorted.reduce((a, b) => a + b, 0) / sorted.length : 0;
819
+ stats.push({
820
+ provider: g.provider,
821
+ model: g.model,
822
+ calls: g.calls,
823
+ errors: g.errors,
824
+ errorRate: g.calls ? g.errors / g.calls : 0,
825
+ ms: {
826
+ p50: Math.round(percentile(sorted, 50)),
827
+ p95: Math.round(percentile(sorted, 95)),
828
+ max: sorted.length ? sorted[sorted.length - 1] : 0,
829
+ mean: Math.round(mean)
830
+ },
831
+ meanTokens: g.tokens.length ? Math.round(g.tokens.reduce((a, b) => a + b, 0) / g.tokens.length) : null,
832
+ reasoningEfforts: Array.from(g.efforts).sort(),
833
+ tools: Array.from(g.tools).sort()
834
+ });
835
+ }
836
+ stats.sort((a, b) => b.ms.p95 - a.ms.p95);
837
+ return stats;
838
+ }
839
+ function aggregateAgreement(records) {
840
+ const groups = /* @__PURE__ */ new Map();
841
+ for (const rec of Array.isArray(records) ? records : []) {
842
+ if (!rec || !Array.isArray(rec.opinions)) continue;
843
+ const finalVerdict = typeof rec.verdict === "string" ? rec.verdict : null;
844
+ for (const op of rec.opinions) {
845
+ if (!op || typeof op.provider !== "string") continue;
846
+ const provider = op.provider;
847
+ const model = typeof op.model === "string" ? op.model : "";
848
+ const key = `${provider}|${model}`;
849
+ let g = groups.get(key);
850
+ if (!g) {
851
+ g = { provider, model, votes: 0, agreed: 0, abstained: 0 };
852
+ groups.set(key, g);
853
+ }
854
+ const opVerdict = typeof op.verdict === "string" ? op.verdict : null;
855
+ if (finalVerdict && opVerdict) {
856
+ g.votes += 1;
857
+ if (opVerdict === finalVerdict) g.agreed += 1;
858
+ } else {
859
+ g.abstained += 1;
860
+ }
861
+ }
862
+ }
863
+ const out = [];
864
+ for (const g of groups.values()) {
865
+ out.push({
866
+ provider: g.provider,
867
+ model: g.model,
868
+ votes: g.votes,
869
+ agreed: g.agreed,
870
+ agreementRate: g.votes ? g.agreed / g.votes : null,
871
+ abstained: g.abstained
872
+ });
873
+ }
874
+ out.sort((a, b) => {
875
+ const ar = a.agreementRate == null ? Infinity : a.agreementRate;
876
+ const br = b.agreementRate == null ? Infinity : b.agreementRate;
877
+ return ar - br;
878
+ });
879
+ return out;
880
+ }
881
+ function detectOutliers(stats) {
882
+ const eligible = (Array.isArray(stats) ? stats : []).filter((s) => s.calls >= MIN_CALLS);
883
+ if (!eligible.length) return [];
884
+ const fastestP95 = Math.min(...eligible.map((s) => s.ms.p95));
885
+ const baseline = Math.max(fastestP95, MIN_BASELINE_MS);
886
+ const out = [];
887
+ for (const s of eligible) {
888
+ if (s.errorRate >= HIGH_ERROR_RATE) {
889
+ out.push({ provider: s.provider, model: s.model, kind: "high-error", detail: `${Math.round(s.errorRate * 100)}% of ${s.calls} calls errored` });
890
+ }
891
+ if (s.ms.p95 >= ABS_SLOW_MS) {
892
+ out.push({ provider: s.provider, model: s.model, kind: "slow-absolute", detail: `p95 ${s.ms.p95}ms (>= ${ABS_SLOW_MS}ms)` });
893
+ } else if (s.ms.p95 >= SLOW_FACTOR * baseline) {
894
+ out.push({ provider: s.provider, model: s.model, kind: "slow-relative", detail: `p95 ${s.ms.p95}ms vs fastest-peer baseline ${Math.round(baseline)}ms` });
895
+ }
896
+ }
897
+ return out;
898
+ }
899
+ function leverFor(provider) {
900
+ if (provider.startsWith(OR_PREFIX)) return { kind: "openrouter", alias: provider.slice(OR_PREFIX.length) };
901
+ if (provider === "codex" || provider === "gemini") return { kind: "external" };
902
+ if (provider === "grok") return { kind: "grok" };
903
+ return { kind: "unknown" };
904
+ }
905
+ function recommend(stats, agreement, config) {
906
+ const cfg = config && typeof config === "object" ? config : {};
907
+ const models = cfg.models && typeof cfg.models === "object" ? cfg.models : {};
908
+ const outliers = detectOutliers(stats);
909
+ const agreeBy = /* @__PURE__ */ new Map();
910
+ for (const a of Array.isArray(agreement) ? agreement : []) agreeBy.set(a.provider, a);
911
+ const out = [];
912
+ let slowOpenRouterCount = 0;
913
+ for (const o of outliers) {
914
+ if (o.kind === "high-error") {
915
+ const lever2 = leverFor(o.provider);
916
+ out.push({
917
+ target: lever2.kind === "openrouter" ? "deliberation" : "external",
918
+ subject: o.provider,
919
+ configKey: lever2.kind === "openrouter" ? `models.${lever2.alias}.askAll` : null,
920
+ action: lever2.kind === "openrouter" ? `set models.${lever2.alias}.askAll=false until it stabilizes` : `check the ${o.provider} credentials/CLI session`,
921
+ rationale: o.detail
922
+ });
923
+ continue;
924
+ }
925
+ const lever = leverFor(o.provider);
926
+ const agree = agreeBy.get(o.provider);
927
+ const rarelyDissents = !!(agree && agree.agreementRate != null && agree.votes >= MIN_VOTES && agree.agreementRate >= HIGH_AGREEMENT);
928
+ const valueNote = rarelyDissents ? ` It also agreed with the final verdict ${agree ? Math.round((agree.agreementRate || 0) * 100) : 0}% of ${agree ? agree.votes : 0} votes (rarely adds dissent), so it is the strongest cut candidate.` : "";
929
+ if (lever.kind === "openrouter") {
930
+ slowOpenRouterCount += 1;
931
+ const alias = typeof lever.alias === "string" ? lever.alias : "";
932
+ const entry = models[alias] && typeof models[alias] === "object" ? models[alias] : null;
933
+ const effort = entry && typeof entry.reasoningEffort === "string" ? entry.reasoningEffort : null;
934
+ if (effort && effort !== "low") {
935
+ out.push({ target: "deliberation", subject: o.provider, configKey: `models.${alias}.reasoningEffort`, action: `lower models.${alias}.reasoningEffort (currently ${effort})`, rationale: `Slowest in the panel (${o.detail}).${valueNote}` });
936
+ }
937
+ out.push({ target: "deliberation", subject: o.provider, configKey: `models.${alias}.askAll`, action: `set models.${alias}.askAll=false to drop it from /ask-all fan-out`, rationale: `In parallel fan-out, wall-time is the slowest model (${o.detail}).${valueNote}` });
938
+ } else if (lever.kind === "external") {
939
+ out.push({ target: "external", subject: o.provider, configKey: null, action: o.provider === "codex" ? "lower model_reasoning_effort in ~/.codex/config.toml (or pass it per-call)" : "lower the Gemini/agy reasoning setting", rationale: `Slowest in the panel (${o.detail}); its reasoning lever is outside deliberation's config.${valueNote}` });
940
+ } else {
941
+ out.push({ target: "deliberation", subject: o.provider, configKey: null, action: `consider whether ${o.provider} earns its latency in the panel`, rationale: `${o.detail}.${valueNote}` });
942
+ }
943
+ }
944
+ if (slowOpenRouterCount >= 2) {
945
+ const fanout = cfg.routing && typeof cfg.routing.maxFanout === "number" ? cfg.routing.maxFanout : null;
946
+ out.push({ target: "deliberation", subject: "panel", configKey: "routing.maxFanout", action: fanout ? `lower routing.maxFanout (currently ${fanout})` : "set routing.maxFanout to 1-2", rationale: `${slowOpenRouterCount} OpenRouter models are slow outliers; a smaller fan-out cuts cost and parallel wall-time.` });
947
+ }
948
+ return out;
949
+ }
950
+ function buildAnalysis(events, records, config, meta) {
951
+ const evs = Array.isArray(events) ? events : [];
952
+ const recs = Array.isArray(records) ? records : [];
953
+ const stats = aggregateByModel(evs);
954
+ const agreement = aggregateAgreement(recs);
955
+ const outliers = detectOutliers(stats);
956
+ const recommendations = recommend(stats, agreement, config);
957
+ return {
958
+ stats,
959
+ agreement,
960
+ outliers,
961
+ recommendations,
962
+ meta: {
963
+ logPath: meta && meta.logPath,
964
+ debugEnabled: !!(meta && meta.debugEnabled),
965
+ sessionsPersist: !!(meta && meta.sessionsPersist),
966
+ eventsParsed: evs.length,
967
+ sessionsRead: recs.length,
968
+ insufficientData: stats.length === 0
969
+ }
970
+ };
971
+ }
972
+ module2.exports = {
973
+ SLOW_FACTOR,
974
+ MIN_CALLS,
975
+ ABS_SLOW_MS,
976
+ HIGH_ERROR_RATE,
977
+ HIGH_AGREEMENT,
978
+ MIN_VOTES,
979
+ parseDebugLog,
980
+ percentile,
981
+ aggregateByModel,
982
+ aggregateAgreement,
983
+ detectOutliers,
984
+ recommend,
985
+ buildAnalysis
986
+ };
987
+ }
988
+ });
989
+
753
990
  // ../../core/sessions.js
754
991
  var require_sessions = __commonJS({
755
992
  "../../core/sessions.js"(exports2, module2) {
@@ -4162,6 +4399,7 @@ var require_openrouter = __commonJS({
4162
4399
  var { makeRegistry, pinAlias } = require_registry();
4163
4400
  var { askAll, askOne, consensus, runToConvergence } = require_orchestrate();
4164
4401
  var { PROMPTS } = require_prompts();
4402
+ var analyzeCore = require_analyze();
4165
4403
  var ADVISORY = { readOnlyHint: true };
4166
4404
  var ASK_PROVIDER = { "ask-gpt": "codex", "ask-gemini": "gemini", "ask-grok": "grok", "ask-openrouter": "openrouter" };
4167
4405
  var EXPERTS = ["architect", "plan-reviewer", "scope-analyst", "code-reviewer", "security-analyst", "researcher", "debugger"];
@@ -4183,6 +4421,15 @@ function panelInputSchema() {
4183
4421
  }
4184
4422
  };
4185
4423
  }
4424
+ function analyzeInputSchema() {
4425
+ return {
4426
+ type: "object",
4427
+ properties: {
4428
+ sessions: { type: "integer", description: "How many recent session records to read for the agreement lens (default 50)." },
4429
+ limitBytes: { type: "integer", description: "Tail size of the debug log to read, in bytes (default 1048576)." }
4430
+ }
4431
+ };
4432
+ }
4186
4433
  function askOneInputSchema() {
4187
4434
  return {
4188
4435
  type: "object",
@@ -4294,7 +4541,8 @@ function toolList() {
4294
4541
  { name: "consensus", description: "Run the FULL multi-round consensus convergence loop server-side with a provider arbiter (blind pass + peer fan-out -> adjudicate -> revise) and return the converged verdict. Default depth is `consensus.maxRounds` (config, default 5); pass `maxRounds` to override. Pass `synthesizeAlways:true` for a SINGLE arbiter synthesis pass instead of the loop (best for open questions, not plan convergence): it returns a free-text `synthesis` and `maxRounds` is ignored. Configure the arbiter via `consensus.arbiter` - a concrete provider/openrouter alias runs server-side; `host` mode returns the opinions for YOU to synthesize. Advisory; pass `expert` to apply a persona. NOTE (Claude Code): use the `/consensus` slash command for the transcript-visible host-arbiter loop (it drives `consensus-step`); this tool is the provider-arbiter path for any host.", inputSchema: consensusInputSchema(), annotations: ADVISORY },
4295
4542
  { name: "consensus-step", description: "Client-driven consensus loop where YOU (the host model) are the arbiter, one step per call: action=init (start, returns sessionId + blind prompt) -> record_blind (your pre-commit verdict) -> dispatch_peers (server fans out to the providers) -> submit_adjudication (your verdict + per-issue accept/dismiss/defer) -> submit_revision (your revised plan), looping until converged or consensus.maxRounds rounds (default 5). State is held server-side by sessionId. Advisory.", inputSchema: consensusStepInputSchema(), annotations: ADVISORY },
4296
4543
  { name: "panel", description: "Return the names of the providers `ask-all` WOULD dispatch for the current config + expert (enabled built-ins + eligible OpenRouter aliases, fanout cap applied), WITHOUT calling them. Use this to discover the panel, then issue one `ask-one` call per provider in parallel for visible per-provider progress. Advisory, read-only.", inputSchema: panelInputSchema(), annotations: ADVISORY },
4297
- { name: "ask-one", description: "Second opinion from ONE named provider in the active panel (e.g. `codex`, `gemini`, `grok`, `openrouter:<alias>` - get the names from `panel`). Returns the standard result envelope. Issue N of these in parallel (one per `panel` name) so each renders independently as it lands. Advisory, single-shot.", inputSchema: askOneInputSchema(), annotations: ADVISORY }
4544
+ { name: "ask-one", description: "Second opinion from ONE named provider in the active panel (e.g. `codex`, `gemini`, `grok`, `openrouter:<alias>` - get the names from `panel`). Returns the standard result envelope. Issue N of these in parallel (one per `panel` name) so each renders independently as it lands. Advisory, single-shot.", inputSchema: askOneInputSchema(), annotations: ADVISORY },
4545
+ { name: "analyze", description: "Analyze recent runs from the opt-in debug log (latency/tokens/reasoning-effort per model) plus the session store (verdict agreement rate), and return advisory tuning suggestions (disable a slow/redundant model in ask-all, lower an OpenRouter model's reasoning, adjust maxFanout). Two lenses reported side by side - timing and agreement are NOT joined (no shared run id). Suggestions are advisory; it writes nothing. Requires `debug.enabled` for the timing lens. Read-only. The `/deliberation:analyze` slash command renders this for humans.", inputSchema: analyzeInputSchema(), annotations: ADVISORY }
4298
4546
  ];
4299
4547
  for (const t of Object.keys(ASK_PROVIDER)) {
4300
4548
  tools.push({ name: t, description: `Single-provider second opinion via ${ASK_PROVIDER[t]} (advisory, single-shot). Pass \`expert\` to apply one of the expert personas.`, inputSchema: inputSchema(), annotations: ADVISORY });
@@ -4789,6 +5037,46 @@ function buildServer({ providers, getConfig, getConfigError, sessionsDir, notify
4789
5037
  return { error: /expected status/.test(msg) ? "unexpected-action-for-status" : "step-failed", detail: msg };
4790
5038
  }
4791
5039
  }
5040
+ function runAnalyze(args) {
5041
+ const fs = require("node:fs");
5042
+ const cfg = getConfig() || {};
5043
+ const dbg = cfg.debug || {};
5044
+ const debugEnabled = !!dbg.enabled;
5045
+ const logPath = typeof dbg.path === "string" && dbg.path || resolveDebugLogPath();
5046
+ const limitBytes = Number.isInteger(args.limitBytes) && args.limitBytes > 0 ? args.limitBytes : 1024 * 1024;
5047
+ let text = "";
5048
+ try {
5049
+ const fd = fs.openSync(logPath, "r");
5050
+ try {
5051
+ const size = fs.fstatSync(fd).size;
5052
+ const start = size > limitBytes ? size - limitBytes : 0;
5053
+ const len = size - start;
5054
+ if (len > 0) {
5055
+ const buf = Buffer.alloc(len);
5056
+ fs.readSync(fd, buf, 0, len, start);
5057
+ text = buf.toString("utf8");
5058
+ if (start > 0) {
5059
+ const nl = text.indexOf("\n");
5060
+ if (nl >= 0) text = text.slice(nl + 1);
5061
+ }
5062
+ }
5063
+ } finally {
5064
+ fs.closeSync(fd);
5065
+ }
5066
+ } catch {
5067
+ }
5068
+ const events = analyzeCore.parseDebugLog(text);
5069
+ const records = [];
5070
+ const persist = persistEnabled();
5071
+ if (persist) {
5072
+ const n = Number.isInteger(args.sessions) && args.sessions > 0 ? args.sessions : 50;
5073
+ for (const e of sessions.listSessions({ dir: sessionsDir }).slice(0, n)) {
5074
+ const rec = sessions.readSession(e.id, { dir: sessionsDir });
5075
+ if (rec) records.push(rec);
5076
+ }
5077
+ }
5078
+ return analyzeCore.buildAnalysis(events, records, cfg, { logPath, debugEnabled, sessionsPersist: persist });
5079
+ }
4792
5080
  async function call(name, args) {
4793
5081
  const namedExpert = EXPERTS.includes(name) ? name : void 0;
4794
5082
  const argExpert = typeof args.expert === "string" ? args.expert : void 0;
@@ -4818,6 +5106,9 @@ function buildServer({ providers, getConfig, getConfigError, sessionsDir, notify
4818
5106
  const result = await askOne(p, withPersona(req, expert), { logger: currentLogger(), tool: "ask-one", cache: resultCache });
4819
5107
  return jsonResult({ result });
4820
5108
  }
5109
+ if (name === "analyze") {
5110
+ return jsonResult(runAnalyze(args));
5111
+ }
4821
5112
  if (name === "ask-all") {
4822
5113
  const { payload, parts } = await runAskAll(req, expert);
4823
5114
  const sid = persistRun("ask-all", req, expert, parts);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@antonbabenko/deliberation-mcp",
3
- "version": "3.3.0",
3
+ "version": "3.5.0",
4
4
  "description": "Deliberation for Claude Code and any MCP host - GPT, Gemini, Grok, and OpenRouter expert subagents.",
5
5
  "mcpName": "io.github.antonbabenko/deliberation",
6
6
  "repository": { "type": "git", "url": "git+https://github.com/antonbabenko/deliberation.git", "directory": "server/mcp" },