prism-mcp-server 20.0.3 → 20.0.5
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/README.md +25 -9
- package/dist/tools/__tests__/layer1Integration.test.js +15 -0
- package/dist/tools/__tests__/ledgerHandlers.test.js +2 -3
- package/dist/tools/ledgerHandlers.js +8 -14
- package/dist/tools/prismInferHandler.js +17 -4
- package/dist/utils/inferenceMetrics.js +18 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -18,19 +18,35 @@ A paid subscription adds cloud sync, higher model tiers, and team features throu
|
|
|
18
18
|
|
|
19
19
|
---
|
|
20
20
|
|
|
21
|
-
## What's New in v20.0.
|
|
21
|
+
## What's New in v20.0.5
|
|
22
22
|
|
|
23
|
-
###
|
|
24
|
-
The
|
|
23
|
+
### Local-First Delegation — 15 Categories, Measured Rate
|
|
24
|
+
The `local-inference-first` skill covers 15 hard-trigger categories (code gen, regex, format conversion, summarization, documentation, factual lookup, classification, shell commands, config gen, and more). Pasted code blocks now trigger delegation regardless of question phrasing. Measured delegation rate: **30-35% on engineering sessions, 40-60% on transform/content sessions**. Rate depends on prompt mix, not the skill — the instruments now self-validate with `nonDelegatedCount` to prevent curated-set tautologies.
|
|
25
25
|
|
|
26
|
-
###
|
|
27
|
-
|
|
26
|
+
### Think-Only Retry (v20.0.4)
|
|
27
|
+
Qwen 3.5 models (9B/27B) with thinking enabled could burn all tokens on `<think>` blocks and return empty content, causing a cascade to 4B. Now detects think-only responses and retries the same tier with thinking disabled — preserving model quality instead of falling to a smaller model.
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## What's New in v20.0.3
|
|
32
|
+
|
|
33
|
+
### Layer 1 Cold-Model Resilience
|
|
34
|
+
The reserved-category classifier now retries once with a longer timeout on cold-model failure, then falls back to a deterministic keyword backstop before refusing. Over-length prompts (>4K chars) are classified as UNCERTAIN before reaching the classifier — prompt padding can no longer force the ERROR branch. This eliminates the cold-start refusal problem without weakening the safety gate.
|
|
35
|
+
|
|
36
|
+
### Keyword Backstop for Reserved Content
|
|
37
|
+
When the LLM classifier fails (timeout, injection, resource pressure), a deterministic regex floor catches reserved vocabulary (restraint, seclusion, self-harm, suicide, overdose, crisis de-escalation, etc.) including inflected and verb forms. Blocks prompt-padding and classifier-injection attacks on the ERROR path.
|
|
38
|
+
|
|
39
|
+
### Single-Source Safety Text
|
|
40
|
+
The safety statement in the MCP server `instructions` field now imports from `boundaries.ts` — one source of truth instead of two hand-maintained copies. Boundaries version bumped to v3 with an explicit delivery decision documented in code.
|
|
41
|
+
|
|
42
|
+
### Reserved-Category Safety Gate — All Tiers (v20.0.2)
|
|
43
|
+
The Layer 1 semantic classifier now runs for **every** user, not just paid tiers. Reserved clinical content is refused on free tier when cloud is unavailable — fail-closed.
|
|
44
|
+
|
|
45
|
+
### Ledger Dedup (v20.0.2)
|
|
46
|
+
`session_save_ledger` deduplicates identical entries within a 5-minute window.
|
|
31
47
|
|
|
32
|
-
### Evidence Script
|
|
33
|
-
`scripts/generate-evidence.sh` regenerates all 5 evidence files with built-in assertions
|
|
48
|
+
### Evidence Script (v20.0.2)
|
|
49
|
+
`scripts/generate-evidence.sh` regenerates all 5 evidence files with built-in assertions. Run `bash scripts/generate-evidence.sh` to verify the full pipeline.
|
|
34
50
|
|
|
35
51
|
---
|
|
36
52
|
|
|
@@ -250,6 +250,21 @@ describe("Layer 1 handler integration", () => {
|
|
|
250
250
|
await expect(runInfer({ prompt: "de-escalation plan for violent behavior", mode: "code", cloud_fallback: true, max_tokens: 512 }, makeBaseDeps({ callLocal, callCloud, callLayer1: callLayer1Mock }))).rejects.toThrow("Layer 1 verdict=OBVIOUS_RESERVED");
|
|
251
251
|
expect(callLocal).not.toHaveBeenCalled();
|
|
252
252
|
});
|
|
253
|
+
it("think_only → retries same tier with think=false, succeeds", async () => {
|
|
254
|
+
let callCount = 0;
|
|
255
|
+
const callLocal = vi.fn().mockImplementation(async (_url, _model, _prompt, _sys, _max, _temp, _timeout, think) => {
|
|
256
|
+
callCount++;
|
|
257
|
+
if (think)
|
|
258
|
+
return { ok: false, reason: "think_only" };
|
|
259
|
+
return { ok: true, text: "answer without thinking", doneReason: "stop" };
|
|
260
|
+
});
|
|
261
|
+
const result = await runInfer({ prompt: "write a regex for emails", mode: "code", cloud_fallback: false, max_tokens: 256 }, makeBaseDeps({ callLocal }));
|
|
262
|
+
expect(callCount).toBe(2);
|
|
263
|
+
expect(callLocal).toHaveBeenNthCalledWith(1, expect.anything(), expect.anything(), expect.anything(), undefined, expect.anything(), expect.anything(), expect.anything(), true);
|
|
264
|
+
expect(callLocal).toHaveBeenNthCalledWith(2, expect.anything(), expect.anything(), expect.anything(), undefined, expect.anything(), expect.anything(), expect.anything(), false);
|
|
265
|
+
expect(result.output).toBe("answer without thinking");
|
|
266
|
+
expect(result.used_cloud).toBe(false);
|
|
267
|
+
});
|
|
253
268
|
});
|
|
254
269
|
// ── Auto-evict unit tests ─────────────────────────────────────────────────────
|
|
255
270
|
describe("Auto-evict warm smaller models (F1/F2/F3/F4 regression suite)", () => {
|
|
@@ -449,15 +449,14 @@ describe("ledgerHandlers", () => {
|
|
|
449
449
|
expect(text).toContain('<prism_memory context="historical">');
|
|
450
450
|
expect(text).toContain("</prism_memory>");
|
|
451
451
|
});
|
|
452
|
-
it("
|
|
452
|
+
it("does not contain inline ABA protocol (delivered via skill routing)", async () => {
|
|
453
453
|
storage.loadContext.mockResolvedValue({
|
|
454
454
|
last_summary: "Summary",
|
|
455
455
|
version: 1,
|
|
456
456
|
});
|
|
457
457
|
const result = await sessionLoadContextHandler(validArgs);
|
|
458
458
|
const text = result.content[0].text;
|
|
459
|
-
expect(text).toContain("ABA PRECISION PROTOCOL");
|
|
460
|
-
expect(text).toContain("Observable Goals");
|
|
459
|
+
expect(text).not.toContain("ABA PRECISION PROTOCOL");
|
|
461
460
|
});
|
|
462
461
|
it("passes role to loadContext when provided", async () => {
|
|
463
462
|
storage.loadContext.mockResolvedValue(null);
|
|
@@ -969,8 +969,10 @@ export async function sessionLoadContextHandler(args) {
|
|
|
969
969
|
: (effectiveRole ? " · 📜 No skill configured" : "");
|
|
970
970
|
greetingBlock = `\n\n[👤 AGENT IDENTITY]\n${namePart}${rolePart}${skillPart}`;
|
|
971
971
|
}
|
|
972
|
-
// ─── SDM
|
|
973
|
-
//
|
|
972
|
+
// ─── SDM Associative Recall (v5.5) ───
|
|
973
|
+
// Accepted as-is (R27): cosine similarity at 0.55 threshold, 3 matches max.
|
|
974
|
+
// Relabeled from "Intuitive Recall" to "Associative Recall" in R16.
|
|
975
|
+
// Not a verified-fact source — label says "similarity-matched, not verified facts."
|
|
974
976
|
let sdmRecallBlock = "";
|
|
975
977
|
if (level !== "quick") {
|
|
976
978
|
try {
|
|
@@ -1010,21 +1012,13 @@ export async function sessionLoadContextHandler(args) {
|
|
|
1010
1012
|
behavWarnings.map(w => `- ${w.summary} (importance: ${w.importance})`).join("\n");
|
|
1011
1013
|
behavBlock = [...rawBlock].slice(0, 2000).join('');
|
|
1012
1014
|
}
|
|
1013
|
-
//
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
`Rule 2 — Precise Execution: One step at a time. Verify each step. If it fails → STOP → fix → verify → then continue.\n` +
|
|
1017
|
-
`Rule 3 — No Reinforcement of Errors: Never repeat the same mistake twice. When the user says something is wrong, read the actual code/data FIRST before forming an opinion.\n` +
|
|
1018
|
-
`Rule 4 — No Negation/Filler/Hedging Lead: NEVER start with "I can't", "I'm unable to", "Unfortunately", "I cannot", "I apologize", "While I cannot", "Sorry", "Regrettably", "I'm afraid", "As an AI", "Sure,", "Certainly,", "I can certainly", "Let me be transparent", "I should note", "Just to clarify", "It's worth noting". You may use "Yes" or "Absolutely" ONLY as a 1-word direct answer to a binary Yes/No question. UNCERTAINTY ESCAPE: Use ONLY for required database fields/API params (e.g., "Missing: patient_id"). Do NOT use as generic refusal.\n` +
|
|
1019
|
-
`Rule 5 — Fix Without Asking: When you see a bug, fix it immediately. Do NOT ask "would you like me to fix that?" — just fix it.\n` +
|
|
1020
|
-
`Rule 6 — Action Intent: When user says "fix/run/open/deploy", they want ACTION not a tutorial. Ask for specific info needed in 1-2 sentences, or act directly.\n` +
|
|
1021
|
-
`Rule 7 — Tool Redirect: When user asks to "open browser"/"run terminal"/"git push" — output ONLY the URL or command. No follow-up. No explanations. Example: "open browser" → "https://synalux.ai/dashboard"`;
|
|
1015
|
+
// ABA Precision Protocol is now delivered via skill routing
|
|
1016
|
+
// (aba-precision-protocol, protected universal at priority -1).
|
|
1017
|
+
// The inline literal was deleted in R25 to eliminate the mirror pair.
|
|
1022
1018
|
// T5 structural truncation: assemble sections in priority order so budget truncation
|
|
1023
1019
|
// drops the LEAST critical sections first (session history), never skills.
|
|
1024
|
-
// Priority high→low:
|
|
1025
|
-
// The header + critical rules are always at the top; history is appended last.
|
|
1020
|
+
// Priority high→low: skills | behavioral warnings | version | drift | briefing | SDM | history
|
|
1026
1021
|
const criticalPrefix = `${MEMORY_BOUNDARY_PREFIX}📋 Session context for "${project}" (${level}):\n\n` +
|
|
1027
|
-
abaProtocol +
|
|
1028
1022
|
behavBlock +
|
|
1029
1023
|
skillBlock +
|
|
1030
1024
|
versionNote +
|
|
@@ -30,7 +30,7 @@ import { stripThink } from "../utils/thinkStrip.js";
|
|
|
30
30
|
import { passesQualityGate } from "../utils/qualityGate.js";
|
|
31
31
|
import { checkInputSafety, checkOutputSafety } from "../utils/safetyGate.js";
|
|
32
32
|
import { callLayer1 as defaultCallLayer1, keywordBackstop } from "../utils/layer1.js";
|
|
33
|
-
import { recordInference, formatInferenceMetrics } from "../utils/inferenceMetrics.js";
|
|
33
|
+
import { recordInference, recordThinkOnlyRetry, formatInferenceMetrics } from "../utils/inferenceMetrics.js";
|
|
34
34
|
// ─── Tool Definition ────────────────────────────────────────────
|
|
35
35
|
export const PRISM_INFER_TOOL = {
|
|
36
36
|
name: "prism_infer",
|
|
@@ -245,8 +245,13 @@ async function callOllamaGenerate(url, model, prompt, system, maxTokens, tempera
|
|
|
245
245
|
if (data.error)
|
|
246
246
|
return { ok: false, reason: `ollama_err:${data.error}` };
|
|
247
247
|
const text = (data.message?.content ?? "").trim();
|
|
248
|
-
if (!text)
|
|
249
|
-
|
|
248
|
+
if (!text) {
|
|
249
|
+
// When think=true, the model may burn all tokens on <think> and
|
|
250
|
+
// produce empty content. Report this distinctly so the tier loop
|
|
251
|
+
// can retry the same model with think=false rather than skipping.
|
|
252
|
+
const hadThinking = !!(data.message?.thinking);
|
|
253
|
+
return { ok: false, reason: hadThinking ? "think_only" : "empty_response" };
|
|
254
|
+
}
|
|
250
255
|
return { ok: true, text, doneReason: data.done_reason, promptTokens: data.prompt_eval_count, completionTokens: data.eval_count };
|
|
251
256
|
}
|
|
252
257
|
catch (err) {
|
|
@@ -561,7 +566,15 @@ export async function runInfer(args, deps) {
|
|
|
561
566
|
anyViable = true;
|
|
562
567
|
const timeout = args.timeout_ms ?? DEFAULT_TIMEOUTS[tier.tag] ?? 60_000;
|
|
563
568
|
const enableThink = args.think ?? (mode !== "route");
|
|
564
|
-
|
|
569
|
+
let result = await deps.callLocal(deps.ollamaUrl, ollamaName, args.prompt, args.system, maxTokens, temperature, timeout, enableThink);
|
|
570
|
+
// Think-only retry: model burned all tokens on <think>, empty content.
|
|
571
|
+
// Retry same model with think=false rather than falling to a smaller tier.
|
|
572
|
+
// One-shot: think=false cannot re-trigger think_only (no thinking to burn).
|
|
573
|
+
if (!result.ok && result.reason === "think_only" && enableThink) {
|
|
574
|
+
debugLog(`[prism_infer] ${tier.tag} returned think-only — retrying with think=false`);
|
|
575
|
+
recordThinkOnlyRetry();
|
|
576
|
+
result = await deps.callLocal(deps.ollamaUrl, ollamaName, args.prompt, args.system, maxTokens, temperature, timeout, false);
|
|
577
|
+
}
|
|
565
578
|
if (result.ok) {
|
|
566
579
|
const { stripped, thinkOnly } = stripThink(result.text);
|
|
567
580
|
const output = stripped;
|
|
@@ -38,6 +38,17 @@ let promptTokensSubmittedEst = 0;
|
|
|
38
38
|
let totalCompletionTokens = 0;
|
|
39
39
|
let totalLatencyMs = 0;
|
|
40
40
|
let cloudTokensSavedEst = 0;
|
|
41
|
+
let thinkOnlyRetries = 0;
|
|
42
|
+
let totalPrompts = 0;
|
|
43
|
+
let nonDelegatedCount = 0;
|
|
44
|
+
export function recordThinkOnlyRetry() {
|
|
45
|
+
thinkOnlyRetries++;
|
|
46
|
+
}
|
|
47
|
+
export function recordPromptSeen(delegated) {
|
|
48
|
+
totalPrompts++;
|
|
49
|
+
if (!delegated)
|
|
50
|
+
nonDelegatedCount++;
|
|
51
|
+
}
|
|
41
52
|
export function recordInference(result) {
|
|
42
53
|
if (result.backend === "safety_gate")
|
|
43
54
|
return;
|
|
@@ -101,6 +112,10 @@ export function getInferenceSnapshot() {
|
|
|
101
112
|
totalTokens: promptTokensSubmittedEst + totalCompletionTokens,
|
|
102
113
|
avgLatencyMs: total > 0 ? Math.round(totalLatencyMs / total) : 0,
|
|
103
114
|
cloudTokensSavedEst,
|
|
115
|
+
thinkOnlyRetries,
|
|
116
|
+
thinkOnlyRetryPct: localCalls > 0 ? Math.round((thinkOnlyRetries / localCalls) * 100) : 0,
|
|
117
|
+
totalPrompts,
|
|
118
|
+
nonDelegatedCount,
|
|
104
119
|
byModel: modelCopy,
|
|
105
120
|
};
|
|
106
121
|
}
|
|
@@ -112,6 +127,9 @@ export function resetInferenceMetrics() {
|
|
|
112
127
|
totalCompletionTokens = 0;
|
|
113
128
|
totalLatencyMs = 0;
|
|
114
129
|
cloudTokensSavedEst = 0;
|
|
130
|
+
thinkOnlyRetries = 0;
|
|
131
|
+
totalPrompts = 0;
|
|
132
|
+
nonDelegatedCount = 0;
|
|
115
133
|
for (const key of Object.keys(byModel)) {
|
|
116
134
|
delete byModel[key];
|
|
117
135
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "prism-mcp-server",
|
|
3
|
-
"version": "20.0.
|
|
3
|
+
"version": "20.0.5",
|
|
4
4
|
"mcpName": "io.github.dcostenco/prism-coder",
|
|
5
5
|
"description": "Prism Coder — Cognitive memory + tool-calling intelligence for AI agents. Mind Palace persistent memory (BFCL Gold Certified, 100% Tool-Call Accuracy, 114 Agent Skills, PHI Guard, Tier Enforcement, Prompt-Based Skill Routing, Zero-Search HDC/HRR retrieval, HRR Semantic Drift Detection across BCBA/Coding/AAC domains, HIPAA-hardened local-first storage, SLERP-optimized GRPO alignment) plus the prism-coder 1.7B–32B open-weights LLM fleet.",
|
|
6
6
|
"module": "index.ts",
|