prism-mcp-server 20.2.0 → 20.2.2
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 +137 -15
- package/dist/cli.js +222 -34
- package/dist/config.js +14 -7
- package/dist/connect.js +854 -16
- package/dist/dashboard/server.js +9 -14
- package/dist/dashboard/settingsPolicy.js +41 -0
- package/dist/localFirstPolicy.js +20 -0
- package/dist/onboarding/wizard.js +3 -6
- package/dist/server.js +77 -65
- package/dist/session/sessionContext.js +5 -3
- package/dist/skillManifestSync.js +943 -0
- package/dist/storage/configStorage.js +110 -1
- package/dist/storage/index.js +15 -4
- package/dist/storage/inferMetricsLedger.js +89 -16
- package/dist/storage/panelMetricsSpool.js +324 -0
- package/dist/storage/sqlite.js +1 -1
- package/dist/storage/synalux.js +18 -1
- package/dist/tools/__tests__/ledgerHandlers.test.js +13 -0
- package/dist/tools/index.js +2 -2
- package/dist/tools/ledgerHandlers.js +468 -53
- package/dist/tools/prismInferHandler.js +171 -12
- package/dist/tools/sessionMemoryDefinitions.js +51 -10
- package/dist/tools/skillRouting.js +39 -7
- package/dist/tools/taskRouterHandler.js +184 -29
- package/dist/utils/entitlements.js +6 -2
- package/dist/utils/inferenceMetrics.js +22 -3
- package/dist/utils/modelPicker.js +3 -3
- package/dist/utils/synaluxJwt.js +8 -3
- package/package.json +6 -2
|
@@ -38,8 +38,8 @@ const CLAW_KEYWORDS = [
|
|
|
38
38
|
"add field", "add column", "add property",
|
|
39
39
|
"remove unused", "delete unused", "clean up",
|
|
40
40
|
];
|
|
41
|
-
/**
|
|
42
|
-
const
|
|
41
|
+
/** Reserved judgment or host-tool boundaries that local inference cannot own. */
|
|
42
|
+
const HOST_BOUNDARY_KEYWORDS = [
|
|
43
43
|
"architect", "architecture", "redesign", "design system",
|
|
44
44
|
"debug complex", "investigate", "root cause", "diagnose",
|
|
45
45
|
"security audit", "vulnerability", "penetration",
|
|
@@ -49,10 +49,56 @@ const HOST_KEYWORDS = [
|
|
|
49
49
|
"migration strategy", "data migration",
|
|
50
50
|
"api design", "schema design", "database design",
|
|
51
51
|
"code review", "review the", "analyze the",
|
|
52
|
-
"
|
|
53
|
-
"complex logic", "algorithm", "concurrent", "race condition",
|
|
52
|
+
"concurrent", "race condition",
|
|
54
53
|
"integrate multiple", "cross-cutting",
|
|
55
|
-
"plan", "strategy", "roadmap",
|
|
54
|
+
"implementation plan", "create a plan", "strategy", "roadmap",
|
|
55
|
+
];
|
|
56
|
+
/** Explicit workflows that require host-side tools or external state. */
|
|
57
|
+
const HOST_TOOL_WORKFLOW_KEYWORDS = [
|
|
58
|
+
"inspect the repository", "search the repository", "read files",
|
|
59
|
+
"run command", "execute command", "run the tests", "run tests",
|
|
60
|
+
"apply the patch", "edit the file", "modify the file",
|
|
61
|
+
"commit", "push", "deploy", "publish",
|
|
62
|
+
"use the browser", "query the database", "database query",
|
|
63
|
+
];
|
|
64
|
+
/**
|
|
65
|
+
* Natural-language action groups that reveal a repository workflow even when
|
|
66
|
+
* the prompt does not use one of the exact phrases above. Local inference can
|
|
67
|
+
* draft a bounded artifact, but it cannot own a read -> mutate -> verify tool
|
|
68
|
+
* sequence. Requiring two distinct groups avoids treating a single request
|
|
69
|
+
* such as "update version" as a host-only workflow.
|
|
70
|
+
*/
|
|
71
|
+
const HOST_TOOL_ACTION_GROUPS = [
|
|
72
|
+
{
|
|
73
|
+
label: "repository inspection",
|
|
74
|
+
patterns: [
|
|
75
|
+
/\b(?:read|inspect|search|open)\b[^.!?\n]{0,100}\b(?:repository|repo|codebase|source|files?|test harness)\b/i,
|
|
76
|
+
],
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
label: "workspace mutation",
|
|
80
|
+
patterns: [
|
|
81
|
+
/\b(?:persist|save|update|edit|modify|apply)\b[^.!?\n]{0,100}\b(?:regression tests?|tests?|files?|code|source|workflow|harness|patch|repository|repo)\b/i,
|
|
82
|
+
],
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
label: "execution or verification",
|
|
86
|
+
patterns: [
|
|
87
|
+
/\b(?:run|execute)\b[^.!?\n]{0,100}\b(?:tests?|test suite|verification|build|type-?check|lint|commands?)\b/i,
|
|
88
|
+
/\b(?:verify|validate)\b[^.!?\n]{0,100}\b(?:tests?|build|behavior|workflow)\b/i,
|
|
89
|
+
],
|
|
90
|
+
},
|
|
91
|
+
];
|
|
92
|
+
/** Complexity signals that should select 27B when the task is bounded. */
|
|
93
|
+
const HIGH_COMPLEXITY_KEYWORDS = [
|
|
94
|
+
"complex logic", "algorithm", "dynamic programming", "constraint solver",
|
|
95
|
+
"parser", "compiler", "state machine", "graph traversal", "backtracking",
|
|
96
|
+
"multiple edge cases", "complete specification",
|
|
97
|
+
];
|
|
98
|
+
/** Evidence that a difficult request is still a self-contained inference job. */
|
|
99
|
+
const BOUNDED_TASK_MARKERS = [
|
|
100
|
+
"self-contained", "standalone", "bounded", "single function", "one function",
|
|
101
|
+
"single file", "one file", "pure function", "complete specification",
|
|
56
102
|
];
|
|
57
103
|
/** Conjunctions and sequential markers that indicate multi-step tasks. */
|
|
58
104
|
const MULTI_STEP_MARKERS = [
|
|
@@ -64,6 +110,15 @@ const MULTI_STEP_MARKERS = [
|
|
|
64
110
|
// Note: removed bare "1.", "2.", "3." — too many false positives
|
|
65
111
|
// on version numbers (v1.2.3), decimals, and IP addresses.
|
|
66
112
|
];
|
|
113
|
+
const MAX_BOUNDED_FILES = 2;
|
|
114
|
+
const MAX_HOST_ROUTABLE_FILES = 5;
|
|
115
|
+
const MULTI_STEP_HOST_THRESHOLD = 2;
|
|
116
|
+
const HOST_TOOL_ACTION_GROUP_THRESHOLD = 2;
|
|
117
|
+
const HIGH_COMPLEXITY_MIN_SCORE = 7;
|
|
118
|
+
const VERY_HIGH_COMPLEXITY_MIN_SCORE = 8;
|
|
119
|
+
const VERY_HIGH_COMPLEXITY_SIGNAL_COUNT = 2;
|
|
120
|
+
const BOUNDED_HIGH_COMPLEXITY_CONFIDENCE = 0.85;
|
|
121
|
+
const HARD_HOST_BOUNDARY_CONFIDENCE = 0.95;
|
|
67
122
|
// ─── Heuristic Engine ────────────────────────────────────────
|
|
68
123
|
/**
|
|
69
124
|
* Count how many keywords from a list appear in the text (case-insensitive).
|
|
@@ -78,19 +133,65 @@ function countKeywordHits(text, keywords) {
|
|
|
78
133
|
}
|
|
79
134
|
return hits;
|
|
80
135
|
}
|
|
136
|
+
function findHostToolActionGroups(description) {
|
|
137
|
+
return HOST_TOOL_ACTION_GROUPS
|
|
138
|
+
.filter(({ patterns }) => patterns.some((pattern) => pattern.test(description)))
|
|
139
|
+
.map(({ label }) => label);
|
|
140
|
+
}
|
|
81
141
|
/**
|
|
82
142
|
* Compute a claw-affinity score from keyword analysis.
|
|
83
143
|
* Returns a value between -1.0 (strongly host) and +1.0 (strongly claw).
|
|
84
144
|
*/
|
|
85
145
|
function keywordSignal(description) {
|
|
86
146
|
const clawHits = countKeywordHits(description, CLAW_KEYWORDS);
|
|
87
|
-
const hostHits = countKeywordHits(description,
|
|
147
|
+
const hostHits = countKeywordHits(description, HOST_BOUNDARY_KEYWORDS);
|
|
88
148
|
const total = clawHits + hostHits;
|
|
89
149
|
if (total === 0)
|
|
90
150
|
return 0; // No signal — neutral
|
|
91
151
|
// Normalized difference: positive = claw, negative = host
|
|
92
152
|
return (clawHits - hostHits) / total;
|
|
93
153
|
}
|
|
154
|
+
/**
|
|
155
|
+
* Decide whether the task is eligible for local inference independently from
|
|
156
|
+
* how much model capacity it needs. A hard host boundary always wins.
|
|
157
|
+
*/
|
|
158
|
+
function assessDelegability(args) {
|
|
159
|
+
const description = args.task_description;
|
|
160
|
+
const files = args.files_involved;
|
|
161
|
+
const boundaryKeywordHits = countKeywordHits(description, HOST_BOUNDARY_KEYWORDS);
|
|
162
|
+
const toolWorkflowHits = countKeywordHits(description, HOST_TOOL_WORKFLOW_KEYWORDS);
|
|
163
|
+
const toolActionGroups = findHostToolActionGroups(description);
|
|
164
|
+
const multiStepHits = countKeywordHits(description, MULTI_STEP_MARKERS);
|
|
165
|
+
const highComplexityHits = countKeywordHits(description, HIGH_COMPLEXITY_KEYWORDS);
|
|
166
|
+
const boundedByFiles = Boolean(files && files.length > 0 && files.length <= MAX_BOUNDED_FILES);
|
|
167
|
+
const boundedByDescription = countKeywordHits(description, BOUNDED_TASK_MARKERS) > 0;
|
|
168
|
+
const reasons = [];
|
|
169
|
+
if (boundaryKeywordHits > 0)
|
|
170
|
+
reasons.push("reserved host judgment");
|
|
171
|
+
if (toolWorkflowHits > 0)
|
|
172
|
+
reasons.push("host tools or external state required");
|
|
173
|
+
if (toolActionGroups.length >= HOST_TOOL_ACTION_GROUP_THRESHOLD) {
|
|
174
|
+
reasons.push(`host workflow actions: ${toolActionGroups.join(", ")}`);
|
|
175
|
+
}
|
|
176
|
+
if (multiStepHits >= MULTI_STEP_HOST_THRESHOLD)
|
|
177
|
+
reasons.push("multi-step workflow");
|
|
178
|
+
if ((files?.length ?? 0) > MAX_HOST_ROUTABLE_FILES)
|
|
179
|
+
reasons.push("cross-file scope");
|
|
180
|
+
if (args.estimated_scope === "refactor")
|
|
181
|
+
reasons.push("refactor scope");
|
|
182
|
+
const minimumComplexity = highComplexityHits >= VERY_HIGH_COMPLEXITY_SIGNAL_COUNT
|
|
183
|
+
? VERY_HIGH_COMPLEXITY_MIN_SCORE
|
|
184
|
+
: highComplexityHits > 0
|
|
185
|
+
? HIGH_COMPLEXITY_MIN_SCORE
|
|
186
|
+
: 1;
|
|
187
|
+
return {
|
|
188
|
+
hardHostBoundary: reasons.length > 0,
|
|
189
|
+
boundedHighComplexity: highComplexityHits > 0 && (boundedByFiles || boundedByDescription),
|
|
190
|
+
highComplexityHits,
|
|
191
|
+
minimumComplexity,
|
|
192
|
+
reasons,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
94
195
|
/**
|
|
95
196
|
* Compute a claw-affinity score from file count.
|
|
96
197
|
* ≤2 files → strongly claw (+1.0)
|
|
@@ -193,6 +294,22 @@ const WEIGHTS = {
|
|
|
193
294
|
length: 0.10,
|
|
194
295
|
multiStep: 0.10,
|
|
195
296
|
};
|
|
297
|
+
/**
|
|
298
|
+
* Forward the deterministic complexity signal without choosing a model.
|
|
299
|
+
* prism_infer owns tier/thinking selection because it also sees the loaded
|
|
300
|
+
* memory size, installed models, live RAM, entitlements, and explicit caller
|
|
301
|
+
* overrides.
|
|
302
|
+
*/
|
|
303
|
+
function buildRecommendedArgs(args, complexityScore) {
|
|
304
|
+
return {
|
|
305
|
+
prompt: args.task_description,
|
|
306
|
+
...(args.project ? { project: args.project } : {}),
|
|
307
|
+
mode: "code",
|
|
308
|
+
task_complexity: complexityScore,
|
|
309
|
+
cloud_fallback: false,
|
|
310
|
+
escalation: "report",
|
|
311
|
+
};
|
|
312
|
+
}
|
|
196
313
|
// ─── Router Core ─────────────────────────────────────────────
|
|
197
314
|
/**
|
|
198
315
|
* Compute the routing recommendation. Pure function.
|
|
@@ -216,6 +333,7 @@ export function computeRoute(args) {
|
|
|
216
333
|
const sc = scopeSignal(estimated_scope);
|
|
217
334
|
const ln = lengthSignal(task_description);
|
|
218
335
|
const ms = multiStepSignal(task_description);
|
|
336
|
+
const delegability = assessDelegability(args);
|
|
219
337
|
// ── Weighted composite score: [-1.0, +1.0] ──
|
|
220
338
|
// Positive = claw-favoring, Negative = host-favoring
|
|
221
339
|
const composite = kw * WEIGHTS.keyword +
|
|
@@ -228,12 +346,20 @@ export function computeRoute(args) {
|
|
|
228
346
|
// composite +1.0 → complexity 1 (trivial)
|
|
229
347
|
// composite -1.0 → complexity 10 (very complex)
|
|
230
348
|
const complexityRaw = Math.round(5.5 - composite * 4.5);
|
|
231
|
-
const complexity_score = Math.max(1, Math.min(10, complexityRaw));
|
|
349
|
+
const complexity_score = Math.max(delegability.minimumComplexity, Math.max(1, Math.min(10, complexityRaw)));
|
|
232
350
|
// ── Determine target ──
|
|
233
|
-
const
|
|
351
|
+
const locallyEligible = composite > 0 || delegability.boundedHighComplexity;
|
|
352
|
+
const isClaw = !delegability.hardHostBoundary &&
|
|
353
|
+
locallyEligible &&
|
|
354
|
+
complexity_score <= PRISM_TASK_ROUTER_MAX_CLAW_COMPLEXITY;
|
|
234
355
|
// ── Confidence: distance from the decision boundary ──
|
|
235
356
|
// Higher absolute composite → higher confidence
|
|
236
|
-
const
|
|
357
|
+
const rawConfidence = Math.min(0.99, Math.round((0.5 + Math.abs(composite) * 0.5) * 100) / 100);
|
|
358
|
+
const confidence = delegability.hardHostBoundary
|
|
359
|
+
? Math.max(rawConfidence, HARD_HOST_BOUNDARY_CONFIDENCE)
|
|
360
|
+
: delegability.boundedHighComplexity
|
|
361
|
+
? Math.max(rawConfidence, BOUNDED_HIGH_COMPLEXITY_CONFIDENCE)
|
|
362
|
+
: rawConfidence;
|
|
237
363
|
// ── Apply confidence threshold ──
|
|
238
364
|
// If confidence is too low, default to host (safer)
|
|
239
365
|
const target = isClaw && confidence >= PRISM_TASK_ROUTER_CONFIDENCE_THRESHOLD ? "claw" : "host";
|
|
@@ -251,6 +377,12 @@ export function computeRoute(args) {
|
|
|
251
377
|
signals.push(`multi-step detected (${ms.toFixed(1)})`);
|
|
252
378
|
if (ln !== 0)
|
|
253
379
|
signals.push(`length signal: ${ln.toFixed(1)}`);
|
|
380
|
+
if (delegability.boundedHighComplexity) {
|
|
381
|
+
signals.push(`bounded high-complexity workload (${delegability.highComplexityHits} signal${delegability.highComplexityHits === 1 ? "" : "s"})`);
|
|
382
|
+
}
|
|
383
|
+
if (delegability.hardHostBoundary) {
|
|
384
|
+
signals.push(`host boundary: ${delegability.reasons.join(", ")}`);
|
|
385
|
+
}
|
|
254
386
|
const rationale = target === "claw"
|
|
255
387
|
? `Task is delegable to the local agent. Signals: ${signals.join("; ") || "neutral"}.`
|
|
256
388
|
: `Task should remain with the host model. Signals: ${signals.join("; ") || "neutral"}.`;
|
|
@@ -259,8 +391,14 @@ export function computeRoute(args) {
|
|
|
259
391
|
confidence,
|
|
260
392
|
complexity_score,
|
|
261
393
|
rationale,
|
|
262
|
-
recommended_tool: target === "claw" ? "
|
|
394
|
+
recommended_tool: target === "claw" ? "prism_infer" : null,
|
|
395
|
+
...(target === "claw" ? {
|
|
396
|
+
recommended_args: buildRecommendedArgs(args, complexity_score),
|
|
397
|
+
} : {}),
|
|
263
398
|
_rawComposite: composite,
|
|
399
|
+
_hardHostBoundary: delegability.hardHostBoundary,
|
|
400
|
+
_boundedHighComplexity: delegability.boundedHighComplexity,
|
|
401
|
+
_minimumComplexity: delegability.minimumComplexity,
|
|
264
402
|
};
|
|
265
403
|
}
|
|
266
404
|
// ─── MCP Handler ─────────────────────────────────────────────
|
|
@@ -282,9 +420,9 @@ export async function sessionTaskRouteHandler(args) {
|
|
|
282
420
|
isError: true,
|
|
283
421
|
};
|
|
284
422
|
}
|
|
285
|
-
//
|
|
286
|
-
//
|
|
287
|
-
const delegationEnabled = await getSetting("delegation_enabled", "
|
|
423
|
+
// Local-first is the product default. An explicit dashboard/config value of
|
|
424
|
+
// "false" remains an operator-owned opt-out and always routes to the host.
|
|
425
|
+
const delegationEnabled = await getSetting("delegation_enabled", "true");
|
|
288
426
|
if (delegationEnabled !== "true") {
|
|
289
427
|
return {
|
|
290
428
|
content: [{
|
|
@@ -293,7 +431,7 @@ export async function sessionTaskRouteHandler(args) {
|
|
|
293
431
|
target: "host",
|
|
294
432
|
confidence: 1.0,
|
|
295
433
|
complexity_score: 5,
|
|
296
|
-
rationale: "
|
|
434
|
+
rationale: "Local delegation was explicitly disabled in Prism settings.",
|
|
297
435
|
recommended_tool: null,
|
|
298
436
|
delegation_enabled: false,
|
|
299
437
|
}),
|
|
@@ -313,14 +451,25 @@ export async function sessionTaskRouteHandler(args) {
|
|
|
313
451
|
const adjustedComposite = Math.max(-1.0, Math.min(1.0, (result._rawComposite || 0) + exp.bias));
|
|
314
452
|
// Recalculate target and complexity if bias flipped the composite sign
|
|
315
453
|
const complexityRaw = Math.round(5.5 - adjustedComposite * 4.5);
|
|
316
|
-
const complexity_score = Math.max(1, Math.min(10, complexityRaw));
|
|
317
|
-
const
|
|
318
|
-
const
|
|
454
|
+
const complexity_score = Math.max(result._minimumComplexity ?? 1, Math.max(1, Math.min(10, complexityRaw)));
|
|
455
|
+
const locallyEligible = adjustedComposite > 0 || result._boundedHighComplexity === true;
|
|
456
|
+
const isClaw = result._hardHostBoundary !== true &&
|
|
457
|
+
locallyEligible &&
|
|
458
|
+
complexity_score <= PRISM_TASK_ROUTER_MAX_CLAW_COMPLEXITY;
|
|
459
|
+
const rawConfidence = Math.min(0.99, Math.round((0.5 + Math.abs(adjustedComposite) * 0.5) * 100) / 100);
|
|
460
|
+
const confidence = result._hardHostBoundary
|
|
461
|
+
? Math.max(rawConfidence, HARD_HOST_BOUNDARY_CONFIDENCE)
|
|
462
|
+
: result._boundedHighComplexity
|
|
463
|
+
? Math.max(rawConfidence, BOUNDED_HIGH_COMPLEXITY_CONFIDENCE)
|
|
464
|
+
: rawConfidence;
|
|
319
465
|
const target = isClaw && confidence >= PRISM_TASK_ROUTER_CONFIDENCE_THRESHOLD ? "claw" : "host";
|
|
320
466
|
result.target = target;
|
|
321
467
|
result.confidence = confidence;
|
|
322
468
|
result.complexity_score = complexity_score;
|
|
323
|
-
result.recommended_tool = target === "claw" ? "
|
|
469
|
+
result.recommended_tool = target === "claw" ? "prism_infer" : null;
|
|
470
|
+
result.recommended_args = target === "claw"
|
|
471
|
+
? buildRecommendedArgs(args, complexity_score)
|
|
472
|
+
: undefined;
|
|
324
473
|
result.experience = {
|
|
325
474
|
bias: exp.bias,
|
|
326
475
|
sample_count: exp.sampleCount,
|
|
@@ -333,8 +482,6 @@ export async function sessionTaskRouteHandler(args) {
|
|
|
333
482
|
// Note: intentionally throwing away the error to keep the original raw heuristic result.
|
|
334
483
|
}
|
|
335
484
|
}
|
|
336
|
-
// Remove the private field from the final output
|
|
337
|
-
delete result._rawComposite;
|
|
338
485
|
// ── v9.x: Local LLM second-opinion for low-confidence cases ──────────────
|
|
339
486
|
// When confidence is below the threshold AND local LLM is enabled,
|
|
340
487
|
// ask prism-coder:9b to break the tie. This is purely additive — if the
|
|
@@ -345,20 +492,28 @@ export async function sessionTaskRouteHandler(args) {
|
|
|
345
492
|
const llmTarget = await askLocalLlmForRoute(args.task_description);
|
|
346
493
|
if (llmTarget) {
|
|
347
494
|
const prev = result.target;
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
495
|
+
const llmCanDelegate = llmTarget === "claw" &&
|
|
496
|
+
result._hardHostBoundary !== true &&
|
|
497
|
+
result.complexity_score <= PRISM_TASK_ROUTER_MAX_CLAW_COMPLEXITY;
|
|
498
|
+
const target = llmCanDelegate ? "claw" : "host";
|
|
499
|
+
result.target = target;
|
|
500
|
+
result.recommended_tool = target === "claw" ? "prism_infer" : null;
|
|
501
|
+
result.recommended_args = target === "claw"
|
|
502
|
+
? buildRecommendedArgs(args, result.complexity_score)
|
|
503
|
+
: undefined;
|
|
354
504
|
result.rationale +=
|
|
355
|
-
` [prism-coder
|
|
505
|
+
` [prism-coder review: heuristic confidence ${result.confidence.toFixed(2)} < threshold → LLM voted "${llmTarget}"; resolved "${target}" (was "${prev}")]`;
|
|
356
506
|
}
|
|
357
507
|
}
|
|
358
508
|
catch {
|
|
359
509
|
// Non-fatal: LLM second-opinion failure never blocks routing
|
|
360
510
|
}
|
|
361
511
|
}
|
|
512
|
+
// Remove internal decision evidence from the public tool response.
|
|
513
|
+
delete result._rawComposite;
|
|
514
|
+
delete result._hardHostBoundary;
|
|
515
|
+
delete result._boundedHighComplexity;
|
|
516
|
+
delete result._minimumComplexity;
|
|
362
517
|
return {
|
|
363
518
|
content: [
|
|
364
519
|
{
|
|
@@ -382,8 +537,8 @@ async function askLocalLlmForRoute(description) {
|
|
|
382
537
|
.replace(/</g, "<").replace(/>/g, ">");
|
|
383
538
|
const prompt = `You are a task routing classifier for an AI coding assistant.\n` +
|
|
384
539
|
`Decision logic:\n` +
|
|
385
|
-
` - "claw":
|
|
386
|
-
` - "host":
|
|
540
|
+
` - "claw": bounded, self-contained, well-defined inference work. It may be difficult if it needs no host tools or reserved judgment.\n` +
|
|
541
|
+
` - "host": architecture, security, investigation, review, multi-step/tool-required workflows, or ambiguous work.\n\n` +
|
|
387
542
|
`CRITICAL: You MUST use the following structural tags:\n` +
|
|
388
543
|
`<|synalux_think|>\n[Internal reasoning about complexity]\n</|synalux_think|>\n\n` +
|
|
389
544
|
`<|tool_call|>\nclaw\n</|tool_call|>\n\n` +
|
|
@@ -68,7 +68,11 @@ export function clampCeiling(requested, planCeiling) {
|
|
|
68
68
|
}
|
|
69
69
|
// ── Fetch ─────────────────────────────────────────────────────────
|
|
70
70
|
async function fetchEntitlements() {
|
|
71
|
-
|
|
71
|
+
// Re-read process.env because dashboard/bootstrap configuration can inject
|
|
72
|
+
// credentials after config.ts captured its module-load constants.
|
|
73
|
+
const baseUrl = process.env.PRISM_SYNALUX_BASE_URL?.trim() || PRISM_SYNALUX_BASE_URL;
|
|
74
|
+
const apiKey = process.env.PRISM_SYNALUX_API_KEY?.trim();
|
|
75
|
+
if ((!SYNALUX_CONFIGURED && !apiKey) || !baseUrl) {
|
|
72
76
|
debugLog("[entitlements] no Synalux auth configured — free tier");
|
|
73
77
|
return { ...FREE_ENTITLEMENTS, source: "unconfigured" };
|
|
74
78
|
}
|
|
@@ -78,7 +82,7 @@ async function fetchEntitlements() {
|
|
|
78
82
|
return { ...FREE_ENTITLEMENTS, source: "fallback_free" };
|
|
79
83
|
}
|
|
80
84
|
try {
|
|
81
|
-
const url = `${
|
|
85
|
+
const url = `${baseUrl}/api/v1/prism/entitlements`;
|
|
82
86
|
const res = await fetch(url, {
|
|
83
87
|
method: "GET",
|
|
84
88
|
headers: { Authorization: `Bearer ${jwt}` },
|
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import { debugLog } from "./logger.js";
|
|
15
15
|
import { appendInferMetric, queryInferMetrics } from "../storage/inferMetricsLedger.js";
|
|
16
|
+
import { ingestPanelMetrics } from "../storage/panelMetricsSpool.js";
|
|
17
|
+
const PANEL_CALLER = "panel";
|
|
16
18
|
// T1 fix: content-aware token estimator. Replaces flat text.length / 4 which
|
|
17
19
|
// underestimates emoji (~2 UTF-16 units but 1.5-2.5 BPE tokens) and CJK
|
|
18
20
|
// (~1 char ≈ 1 token) by 15-40%, and overestimates dense code (~3.3 chars/token).
|
|
@@ -169,9 +171,13 @@ export function resetInferenceMetrics() {
|
|
|
169
171
|
}
|
|
170
172
|
export async function inferenceMetricsHandler(args) {
|
|
171
173
|
if (args?.period === "all") {
|
|
174
|
+
const ingest = await ingestPanelMetrics();
|
|
172
175
|
const agg = await queryInferMetrics();
|
|
173
176
|
if (!agg || agg.total === 0) {
|
|
174
|
-
|
|
177
|
+
const warning = ingest.failed_files > 0
|
|
178
|
+
? ` Panel spool ingestion failed for ${ingest.failed_files} file(s); retained for retry.`
|
|
179
|
+
: "";
|
|
180
|
+
return { content: [{ type: "text", text: `No persisted inference calls yet (ledger empty).${warning}` }] };
|
|
175
181
|
}
|
|
176
182
|
const localPct = agg.total ? Math.round((agg.local / agg.total) * 100) : 0;
|
|
177
183
|
const cloudPct = agg.total ? Math.round((agg.cloud / agg.total) * 100) : 0;
|
|
@@ -181,13 +187,26 @@ export async function inferenceMetricsHandler(args) {
|
|
|
181
187
|
const byB = Object.entries(agg.by_backend)
|
|
182
188
|
.sort((a, b) => b[1] - a[1])
|
|
183
189
|
.map(([k, v]) => ` ${k}: ${v}`).join("\n");
|
|
190
|
+
const panel = agg.by_caller[PANEL_CALLER];
|
|
191
|
+
const panelPct = panel?.total ? Math.round((panel.local / panel.total) * 100) : 0;
|
|
192
|
+
const panelLine = panel
|
|
193
|
+
? `Panel local serve rate: ${panelPct}% (${panel.local}/${panel.total}; cloud ${panel.cloud})`
|
|
194
|
+
: "Panel local serve rate: no panel calls recorded";
|
|
195
|
+
let ingestNote = "";
|
|
196
|
+
if (ingest.invalid > 0)
|
|
197
|
+
ingestNote = `discarded ${ingest.invalid} invalid panel row(s)`;
|
|
198
|
+
if (ingest.failed_files > 0) {
|
|
199
|
+
ingestNote += `${ingestNote ? "; " : ""}retained ${ingest.failed_files} panel file(s) for retry`;
|
|
200
|
+
}
|
|
201
|
+
const ingestLine = ingestNote ? `\nPanel spool: ${ingestNote}` : "";
|
|
184
202
|
return {
|
|
185
203
|
content: [{
|
|
186
204
|
type: "text",
|
|
187
|
-
text: `📊
|
|
205
|
+
text: `📊 Inference Metrics — ALL TIME (persisted ledger, ${span})\n` +
|
|
188
206
|
`Total calls: ${agg.total} — Local: ${agg.local} (${localPct}%) | Cloud: ${agg.cloud} (${cloudPct}%)\n` +
|
|
207
|
+
`${panelLine}\n` +
|
|
189
208
|
`Prompt tokens: ${agg.prompt_tokens} | Completion tokens: ${agg.completion_tokens}\n` +
|
|
190
|
-
`Avg latency: ${agg.avg_latency_ms}ms\nBy backend:\n${byB}`,
|
|
209
|
+
`Avg latency: ${agg.avg_latency_ms}ms\nBy backend:\n${byB}${ingestLine}`,
|
|
191
210
|
}],
|
|
192
211
|
};
|
|
193
212
|
}
|
|
@@ -13,10 +13,10 @@
|
|
|
13
13
|
* This saves 11GB+ RAM vs 27b and keeps response times fast.
|
|
14
14
|
*
|
|
15
15
|
* tag weights need free ctx role
|
|
16
|
-
* prism-coder:27b ~16 GB ≥ 20 GB
|
|
17
|
-
* prism-coder:9b ~ 5.8 GB ≥ 8 GB
|
|
16
|
+
* prism-coder:27b ~16 GB ≥ 20 GB 4K quality (on-demand, Qwen3.5 DeltaNet, 100% BFCL)
|
|
17
|
+
* prism-coder:9b ~ 5.8 GB ≥ 8 GB 4K default router (Qwen3.5, 100% BFCL)
|
|
18
18
|
* prism-coder:4b ~ 3.4 GB ≥ 5 GB 32K verifier (Qwen3.5, 100%)
|
|
19
|
-
* prism-coder:2b ~ 2.3 GB ≥ 3 GB
|
|
19
|
+
* prism-coder:2b ~ 2.3 GB ≥ 3 GB 32K mobile / iPhone (Qwen3.5, 99.1%)
|
|
20
20
|
*
|
|
21
21
|
* Below 3 GB free → no local pick (caller must use cloud).
|
|
22
22
|
*/
|
package/dist/utils/synaluxJwt.js
CHANGED
|
@@ -32,7 +32,12 @@ let inFlight = null;
|
|
|
32
32
|
* Concurrent callers share a single in-flight exchange (no thundering herd).
|
|
33
33
|
*/
|
|
34
34
|
export async function getSynaluxJwt() {
|
|
35
|
-
|
|
35
|
+
// Re-read process.env because storage/dashboard configuration can inject
|
|
36
|
+
// credentials after config.ts captured its module-load constants.
|
|
37
|
+
const baseUrl = process.env.PRISM_SYNALUX_BASE_URL?.trim() ||
|
|
38
|
+
process.env.SYNALUX_BASE_URL?.trim() || PRISM_SYNALUX_BASE_URL;
|
|
39
|
+
const apiKey = process.env.PRISM_SYNALUX_API_KEY?.trim() || PRISM_SYNALUX_API_KEY;
|
|
40
|
+
if (!baseUrl || !apiKey) {
|
|
36
41
|
return null;
|
|
37
42
|
}
|
|
38
43
|
const now = Date.now();
|
|
@@ -43,11 +48,11 @@ export async function getSynaluxJwt() {
|
|
|
43
48
|
return inFlight;
|
|
44
49
|
inFlight = (async () => {
|
|
45
50
|
try {
|
|
46
|
-
const url = `${
|
|
51
|
+
const url = `${baseUrl}/api/v1/auth/jwt`;
|
|
47
52
|
const res = await fetch(url, {
|
|
48
53
|
method: "POST",
|
|
49
54
|
headers: {
|
|
50
|
-
"Authorization": `Bearer ${
|
|
55
|
+
"Authorization": `Bearer ${apiKey}`,
|
|
51
56
|
"Content-Type": "application/json",
|
|
52
57
|
},
|
|
53
58
|
signal: AbortSignal.timeout(10_000),
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "prism-mcp-server",
|
|
3
|
-
"version": "20.2.
|
|
3
|
+
"version": "20.2.2",
|
|
4
4
|
"mcpName": "io.github.dcostenco/prism-coder",
|
|
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-
|
|
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 or subscription-gated Synalux storage, SLERP-optimized GRPO alignment) plus the prism-coder 1.7B–32B open-weights LLM fleet.",
|
|
6
6
|
"module": "index.ts",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"main": "dist/server.js",
|
|
@@ -16,6 +16,8 @@
|
|
|
16
16
|
"dist"
|
|
17
17
|
],
|
|
18
18
|
"scripts": {
|
|
19
|
+
"clean": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\"",
|
|
20
|
+
"prebuild": "npm run clean",
|
|
19
21
|
"build": "tsc && npm run chmod-bins",
|
|
20
22
|
"chmod-bins": "node -e \"['dist/cli.js','dist/server.js','dist/utils/universalImporter.js'].forEach(f => { try { require('fs').chmodSync(f, 0o755); } catch (e) { console.warn('chmod skipped', f, e.message); } })\"",
|
|
21
23
|
"prepublishOnly": "npm run build",
|
|
@@ -25,6 +27,8 @@
|
|
|
25
27
|
"test:watch": "vitest",
|
|
26
28
|
"test:load": "vitest run tests/load/",
|
|
27
29
|
"test:ci": "vitest run --reporter=junit --outputFile=test-results.xml",
|
|
30
|
+
"test:routing:live": "npm run build && node scripts/prism-infer-live-test.mjs",
|
|
31
|
+
"test:routing:models": "npm run build && node scripts/prism-infer-live-test.mjs --infer",
|
|
28
32
|
"test:mcp": "node ./test_cross_mcp.js",
|
|
29
33
|
"import": "node dist/utils/universalImporter.js"
|
|
30
34
|
},
|