@klhapp/skillmux 1.6.0 → 1.7.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/CHANGELOG.md +14 -0
- package/README.md +7 -7
- package/config.example.toml +5 -0
- package/config.remote.example.toml +4 -7
- package/docs/README.md +4 -4
- package/docs/assets/architecture.svg +1 -1
- package/docs/cli.md +4 -57
- package/docs/concepts.md +12 -14
- package/docs/configuration.md +10 -11
- package/docs/deployment.md +11 -8
- package/docs/getting-started.md +1 -1
- package/docs/mcp-routing.md +28 -23
- package/docs/ranked-shortlist-migration.md +160 -0
- package/docs/schema.json +37 -139
- package/docs/skill-management.md +7 -2
- package/docs/troubleshooting.md +12 -14
- package/package.json +1 -1
- package/src/adapters.ts +1 -536
- package/src/audit.ts +1 -3
- package/src/cli.ts +19 -254
- package/src/completions.ts +0 -4
- package/src/config-service.ts +19 -32
- package/src/config-watcher.ts +2 -6
- package/src/config.ts +61 -60
- package/src/db.ts +32 -18
- package/src/doctor.ts +1 -84
- package/src/eval.ts +143 -60
- package/src/init.ts +4 -5
- package/src/metrics.ts +1 -13
- package/src/router-core.ts +42 -149
- package/src/server.ts +13 -27
- package/src/stats.ts +126 -57
- package/src/types.ts +8 -39
- package/docs/calibration.md +0 -198
- package/src/calibrate.ts +0 -1775
- package/src/config-mutation.ts +0 -65
- package/src/dataset-generator.ts +0 -119
- package/src/decision.ts +0 -45
package/src/config-mutation.ts
DELETED
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
import { writeFileSync, renameSync } from "node:fs";
|
|
2
|
-
|
|
3
|
-
export interface ThresholdsPatchOptions {
|
|
4
|
-
matchScore: number;
|
|
5
|
-
matchMargin: number;
|
|
6
|
-
candidateFloor: number;
|
|
7
|
-
runId: string;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Remove a TOML section header and all lines until the next section header
|
|
12
|
-
* (or end of file). Matches exact header string at start of line.
|
|
13
|
-
*/
|
|
14
|
-
export function removeSectionBlock(source: string, header: string): string {
|
|
15
|
-
const lines = source.split("\n");
|
|
16
|
-
const out: string[] = [];
|
|
17
|
-
let skipping = false;
|
|
18
|
-
for (const line of lines) {
|
|
19
|
-
const trimmed = line.trimEnd();
|
|
20
|
-
if (trimmed === header || trimmed.startsWith(header + " ")) {
|
|
21
|
-
skipping = true;
|
|
22
|
-
continue;
|
|
23
|
-
}
|
|
24
|
-
if (skipping && line.trimStart().startsWith("[")) {
|
|
25
|
-
skipping = false;
|
|
26
|
-
}
|
|
27
|
-
if (!skipping) out.push(line);
|
|
28
|
-
}
|
|
29
|
-
return out.join("\n");
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Surgically patch a TOML string to set [inference.thresholds] values and
|
|
34
|
-
* [inference.calibration] run_id while preserving unrelated sections and comments.
|
|
35
|
-
*/
|
|
36
|
-
export function patchToml(
|
|
37
|
-
source: string,
|
|
38
|
-
opts: ThresholdsPatchOptions,
|
|
39
|
-
): string {
|
|
40
|
-
const thresholdsBlock = `[inference.thresholds]\nmatch_score = ${opts.matchScore}\nmatch_margin = ${opts.matchMargin}\ncandidate_floor = ${opts.candidateFloor}\n`;
|
|
41
|
-
const calibrationBlock = `[inference.calibration]\nrun_id = "${opts.runId}"\n`;
|
|
42
|
-
|
|
43
|
-
// Remove any existing [inference.thresholds] and [inference.calibration] sections
|
|
44
|
-
let result = removeSectionBlock(source, "[inference.thresholds]");
|
|
45
|
-
result = removeSectionBlock(result, "[inference.calibration]");
|
|
46
|
-
|
|
47
|
-
// Append both sections cleanly
|
|
48
|
-
result = result.trimEnd() + "\n\n" + thresholdsBlock + "\n" + calibrationBlock;
|
|
49
|
-
return result;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Atomically write patched TOML to disk via temp file write and renameSync.
|
|
54
|
-
*/
|
|
55
|
-
export async function patchTomlFile(
|
|
56
|
-
tomlPath: string,
|
|
57
|
-
opts: ThresholdsPatchOptions,
|
|
58
|
-
): Promise<void> {
|
|
59
|
-
const existing = await Bun.file(tomlPath).text();
|
|
60
|
-
const patched = patchToml(existing, opts);
|
|
61
|
-
|
|
62
|
-
const tmpPath = `${tomlPath}.${process.pid}.tmp`;
|
|
63
|
-
writeFileSync(tmpPath, patched);
|
|
64
|
-
renameSync(tmpPath, tomlPath);
|
|
65
|
-
}
|
package/src/dataset-generator.ts
DELETED
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
import type { DecisionSplit, DecisionOutcome } from "./calibrate";
|
|
2
|
-
import type { VaultSkill } from "./vault";
|
|
3
|
-
|
|
4
|
-
export interface RawDecisionCase {
|
|
5
|
-
query: string;
|
|
6
|
-
split: DecisionSplit;
|
|
7
|
-
expected_outcome: DecisionOutcome;
|
|
8
|
-
relevant_skill_ids: string[];
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export interface GenerateDatasetOptions {
|
|
12
|
-
/** Target number of queries per split. Default: 10. */
|
|
13
|
-
queriesPerSplit?: number;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
const STOP_WORDS = new Set([
|
|
17
|
-
"a", "an", "and", "are", "as", "at", "be", "by", "for", "from", "in", "is",
|
|
18
|
-
"it", "of", "on", "or", "the", "this", "to", "use", "with",
|
|
19
|
-
]);
|
|
20
|
-
|
|
21
|
-
function words(value: string): string[] {
|
|
22
|
-
return value
|
|
23
|
-
.toLowerCase()
|
|
24
|
-
.match(/[a-z0-9]+/g)
|
|
25
|
-
?.filter((word) => word.length > 2 && !STOP_WORDS.has(word)) ?? [];
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function anchors(skill: VaultSkill): string[] {
|
|
29
|
-
const preferred = [...skill.aliases.flatMap(words), ...words(skill.title)];
|
|
30
|
-
const fallback = words(skill.description);
|
|
31
|
-
return [...new Set([...preferred, ...fallback])].slice(0, 2);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function matchedQuery(skill: VaultSkill, variant: number): string {
|
|
35
|
-
const [first = "specialized", second = "workflow"] = anchors(skill);
|
|
36
|
-
const templates = [
|
|
37
|
-
`I need practical guidance completing an unfamiliar ${first} ${second} task safely`,
|
|
38
|
-
`Which available workflow can handle my unusual ${first} ${second} problem end to end`,
|
|
39
|
-
`Please guide me through a difficult unfamiliar ${first} ${second} operation safely`,
|
|
40
|
-
];
|
|
41
|
-
return templates[variant % templates.length]!;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function ambiguousQuery(first: VaultSkill, second: VaultSkill): string {
|
|
45
|
-
const [firstAnchor = "first"] = anchors(first);
|
|
46
|
-
const [secondAnchor = "second"] = anchors(second);
|
|
47
|
-
return `Help with a workflow spanning both ${firstAnchor} and ${secondAnchor} responsibilities`;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function nearMissQuery(first: VaultSkill, second: VaultSkill): string {
|
|
51
|
-
const [firstAnchor = "one"] = anchors(first);
|
|
52
|
-
const [secondAnchor = "another"] = anchors(second);
|
|
53
|
-
return `Explain the theory comparing ${firstAnchor} and ${secondAnchor} without performing either workflow`;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Automatically generate a synthetic decision-policy calibration dataset
|
|
58
|
-
* from local vault skill definitions. Runs 100% locally with zero data leaks.
|
|
59
|
-
*/
|
|
60
|
-
export function generateDataset(
|
|
61
|
-
skills: VaultSkill[],
|
|
62
|
-
options: GenerateDatasetOptions = {},
|
|
63
|
-
): RawDecisionCase[] {
|
|
64
|
-
if (skills.length < 4) {
|
|
65
|
-
throw new Error(
|
|
66
|
-
"Dataset generation requires at least 4 vault skills so tune and test can each contain matched and ambiguous cases without skill leakage",
|
|
67
|
-
);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
const cases: RawDecisionCase[] = [];
|
|
71
|
-
const sorted = [...skills].sort((a, b) => a.skill_id.localeCompare(b.skill_id));
|
|
72
|
-
const splitAt = Math.ceil(sorted.length / 2);
|
|
73
|
-
const bySplit: Record<DecisionSplit, VaultSkill[]> = {
|
|
74
|
-
tune: sorted.slice(0, splitAt),
|
|
75
|
-
test: sorted.slice(splitAt),
|
|
76
|
-
};
|
|
77
|
-
const targetPerSplit = Math.max(3, options.queriesPerSplit ?? 10);
|
|
78
|
-
|
|
79
|
-
for (const split of ["tune", "test"] as const) {
|
|
80
|
-
const splitSkills = bySplit[split];
|
|
81
|
-
for (let i = 0; i < splitSkills.length; i++) {
|
|
82
|
-
const skill = splitSkills[i]!;
|
|
83
|
-
cases.push({
|
|
84
|
-
query: matchedQuery(skill, i),
|
|
85
|
-
split,
|
|
86
|
-
expected_outcome: "matched",
|
|
87
|
-
relevant_skill_ids: [skill.skill_id],
|
|
88
|
-
});
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
const first = splitSkills[0]!;
|
|
92
|
-
const second = splitSkills[1]!;
|
|
93
|
-
cases.push({
|
|
94
|
-
query: ambiguousQuery(first, second),
|
|
95
|
-
split,
|
|
96
|
-
expected_outcome: "ambiguous",
|
|
97
|
-
relevant_skill_ids: [first.skill_id, second.skill_id],
|
|
98
|
-
});
|
|
99
|
-
cases.push({
|
|
100
|
-
query: nearMissQuery(first, second),
|
|
101
|
-
split,
|
|
102
|
-
expected_outcome: "no_match",
|
|
103
|
-
relevant_skill_ids: [],
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
for (let i = cases.filter((item) => item.split === split).length; i < targetPerSplit; i++) {
|
|
107
|
-
const left = splitSkills[i % splitSkills.length]!;
|
|
108
|
-
const right = splitSkills[(i + 1) % splitSkills.length]!;
|
|
109
|
-
cases.push({
|
|
110
|
-
query: i % 2 === 0 ? matchedQuery(left, i) : nearMissQuery(left, right),
|
|
111
|
-
split,
|
|
112
|
-
expected_outcome: i % 2 === 0 ? "matched" : "no_match",
|
|
113
|
-
relevant_skill_ids: i % 2 === 0 ? [left.skill_id] : [],
|
|
114
|
-
});
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
return cases;
|
|
119
|
-
}
|
package/src/decision.ts
DELETED
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
import type { RankedCandidate, Thresholds } from "./types";
|
|
2
|
-
|
|
3
|
-
export interface DecisionInput {
|
|
4
|
-
reranked: boolean;
|
|
5
|
-
candidates: RankedCandidate[];
|
|
6
|
-
thresholds: Thresholds;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export type Decision =
|
|
10
|
-
| { outcome: "matched"; skill_id: string; score: number; margin: number }
|
|
11
|
-
| { outcome: "ambiguous"; candidates: RankedCandidate[] }
|
|
12
|
-
| { outcome: "no_match" };
|
|
13
|
-
|
|
14
|
-
export function decideResolveOutcome({ reranked, candidates, thresholds }: DecisionInput): Decision {
|
|
15
|
-
if (candidates.length === 0) return { outcome: "no_match" };
|
|
16
|
-
|
|
17
|
-
// Degraded lane: no comparable scores exist, so never match; the (BM25-ordered)
|
|
18
|
-
// Without calibrated reranker scores, the shortlist goes to the calling LLM.
|
|
19
|
-
if (!reranked) return { outcome: "ambiguous", candidates: candidates.slice(0, thresholds.candidate_limit) };
|
|
20
|
-
|
|
21
|
-
if (
|
|
22
|
-
thresholds.match_score === undefined
|
|
23
|
-
|| thresholds.match_margin === undefined
|
|
24
|
-
|| thresholds.candidate_floor === undefined
|
|
25
|
-
) {
|
|
26
|
-
return {
|
|
27
|
-
outcome: "ambiguous",
|
|
28
|
-
candidates: candidates.slice(0, thresholds.candidate_limit),
|
|
29
|
-
};
|
|
30
|
-
}
|
|
31
|
-
const { match_score, match_margin, candidate_floor } = thresholds;
|
|
32
|
-
|
|
33
|
-
const sorted = [...candidates].sort((a, b) => (b.score ?? -Infinity) - (a.score ?? -Infinity));
|
|
34
|
-
const eligible = sorted.filter((c) => (c.score ?? -Infinity) >= candidate_floor);
|
|
35
|
-
if (eligible.length === 0) return { outcome: "no_match" };
|
|
36
|
-
|
|
37
|
-
const top = eligible[0]!;
|
|
38
|
-
const topScore = top.score!;
|
|
39
|
-
const margin = sorted.length === 1 ? topScore : topScore - (sorted[1]!.score ?? 0);
|
|
40
|
-
|
|
41
|
-
if (topScore >= match_score && margin >= match_margin) {
|
|
42
|
-
return { outcome: "matched", skill_id: top.skill_id, score: topScore, margin };
|
|
43
|
-
}
|
|
44
|
-
return { outcome: "ambiguous", candidates: eligible.slice(0, thresholds.candidate_limit) };
|
|
45
|
-
}
|