@aeondave/pi-skill-optimizer 3.0.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/BENCHMARK.md +354 -0
- package/CHANGELOG.md +117 -0
- package/LICENSE +21 -0
- package/README.md +148 -0
- package/assets/banner.png +0 -0
- package/docs/ARCHITECTURE.md +66 -0
- package/docs/CONFIGURATION.md +138 -0
- package/docs/RANKING.md +155 -0
- package/package.json +85 -0
- package/src/activation.ts +88 -0
- package/src/aliases.ts +111 -0
- package/src/config.ts +446 -0
- package/src/corpus.ts +416 -0
- package/src/evaluation.ts +692 -0
- package/src/generate.ts +365 -0
- package/src/history.ts +147 -0
- package/src/index.ts +1362 -0
- package/src/init-profile.ts +96 -0
- package/src/init-prompt.ts +10 -0
- package/src/init-readiness.ts +123 -0
- package/src/optimize.ts +424 -0
- package/src/output-recovery.ts +19 -0
- package/src/output.ts +629 -0
- package/src/persistence.ts +535 -0
- package/src/profile.ts +253 -0
- package/src/prompts/init-profile.md +40 -0
- package/src/request.ts +304 -0
- package/src/skill-loader.ts +138 -0
- package/src/skills.ts +1230 -0
- package/src/stats.ts +178 -0
- package/src/tools.ts +548 -0
- package/src/usage.ts +256 -0
package/src/usage.ts
ADDED
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import { normalizeRequest } from "./request.ts";
|
|
2
|
+
|
|
3
|
+
export interface SkillUsageEntry {
|
|
4
|
+
count: number;
|
|
5
|
+
lastUsed: number;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export type SkillUsageStats = Record<string, SkillUsageEntry>;
|
|
9
|
+
|
|
10
|
+
export interface SkillUsageFile {
|
|
11
|
+
version: 1;
|
|
12
|
+
updatedAt: string;
|
|
13
|
+
skills: SkillUsageStats;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface SkillUsageEvidence {
|
|
17
|
+
key: string;
|
|
18
|
+
name: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const DEFAULT_USAGE_MAX_ENTRIES = 2_048;
|
|
22
|
+
export const DEFAULT_USAGE_STALE_DAYS = 180;
|
|
23
|
+
export const STALE_LOW_USAGE_MAX_COUNT = 1;
|
|
24
|
+
|
|
25
|
+
export interface UsagePruneOptions {
|
|
26
|
+
/** Zero disables the hard cap. */
|
|
27
|
+
maxEntries?: number;
|
|
28
|
+
/** Zero disables stale one-off pruning. */
|
|
29
|
+
staleDays?: number;
|
|
30
|
+
/** Critical, explicitly full, or currently pinned skills. */
|
|
31
|
+
protectedNames?: readonly string[];
|
|
32
|
+
now?: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function isInjectedContextText(text: string): boolean {
|
|
36
|
+
return text.trimStart().startsWith("context-mode active.");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function escapeRegExp(value: string): string {
|
|
40
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function isMentioned(text: string, name: string): boolean {
|
|
44
|
+
const escaped = escapeRegExp(name.toLowerCase());
|
|
45
|
+
return new RegExp(`(^|[^a-z0-9:_-])(?:/skill:|/)?${escaped}($|[^a-z0-9:_-])`).test(text);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function stableHash(value: string): string {
|
|
49
|
+
let hash = 0x811c9dc5;
|
|
50
|
+
for (let i = 0; i < value.length; i++) {
|
|
51
|
+
hash ^= value.charCodeAt(i);
|
|
52
|
+
hash = Math.imul(hash, 0x01000193);
|
|
53
|
+
}
|
|
54
|
+
return (hash >>> 0).toString(36);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function objectId(value: Record<string, unknown>): string | undefined {
|
|
58
|
+
for (const key of ["id", "messageId", "message_id", "toolCallId", "tool_call_id", "toolUseId", "tool_use_id"]) {
|
|
59
|
+
const id = value[key];
|
|
60
|
+
if ((typeof id === "string" || typeof id === "number") && String(id)) return String(id);
|
|
61
|
+
}
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Stable per-message/tool-call evidence, suitable for bounded session deduplication. */
|
|
66
|
+
export function collectSkillUsageEvidence(payload: unknown, selectedSkills: readonly string[]): SkillUsageEvidence[] {
|
|
67
|
+
const normalizedMessages = normalizeRequest(payload).messages;
|
|
68
|
+
const selected = new Map(selectedSkills.filter(Boolean).map((name) => [name.toLowerCase(), name]));
|
|
69
|
+
const evidence = new Map<string, SkillUsageEvidence>();
|
|
70
|
+
for (let messageIndex = 0; messageIndex < normalizedMessages.length; messageIndex++) {
|
|
71
|
+
const message = normalizedMessages[messageIndex];
|
|
72
|
+
if (message.role !== "user") continue;
|
|
73
|
+
const text = isInjectedContextText(message.text) ? "" : message.text.toLowerCase();
|
|
74
|
+
if (!text) continue;
|
|
75
|
+
const source = message.sourceId === undefined
|
|
76
|
+
? `index:${messageIndex}:${stableHash(text)}`
|
|
77
|
+
: `id:${message.sourceId}`;
|
|
78
|
+
for (const [lower, name] of selected) {
|
|
79
|
+
if (!isMentioned(text, lower)) continue;
|
|
80
|
+
const key = `user:${source}:${lower}`;
|
|
81
|
+
evidence.set(key, { key, name });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const rawMessages = Array.isArray(payload)
|
|
86
|
+
? payload
|
|
87
|
+
: payload && typeof payload === "object" && Array.isArray((payload as { messages?: unknown }).messages)
|
|
88
|
+
? (payload as { messages: unknown[] }).messages
|
|
89
|
+
: [];
|
|
90
|
+
for (let messageIndex = 0; messageIndex < rawMessages.length; messageIndex++) {
|
|
91
|
+
const rawMessage = rawMessages[messageIndex];
|
|
92
|
+
if (!rawMessage || typeof rawMessage !== "object") continue;
|
|
93
|
+
const message = rawMessage as Record<string, unknown>;
|
|
94
|
+
if (!Array.isArray(message.content)) continue;
|
|
95
|
+
for (let blockIndex = 0; blockIndex < message.content.length; blockIndex++) {
|
|
96
|
+
const rawBlock = message.content[blockIndex];
|
|
97
|
+
if (!rawBlock || typeof rawBlock !== "object") continue;
|
|
98
|
+
const block = rawBlock as Record<string, unknown>;
|
|
99
|
+
if (block.type !== "tool_use" || typeof block.name !== "string") continue;
|
|
100
|
+
const toolName = block.name.toLowerCase();
|
|
101
|
+
if (toolName !== "skill" && toolName !== "activate_skill") continue;
|
|
102
|
+
if (!block.input || typeof block.input !== "object") continue;
|
|
103
|
+
const input = block.input as Record<string, unknown>;
|
|
104
|
+
let serialized = "";
|
|
105
|
+
try { serialized = JSON.stringify(input); } catch { serialized = String(input); }
|
|
106
|
+
const sourceId = objectId(block);
|
|
107
|
+
const source = sourceId === undefined
|
|
108
|
+
? `index:${messageIndex}:${blockIndex}:${stableHash(serialized)}`
|
|
109
|
+
: `id:${sourceId}`;
|
|
110
|
+
for (const inputKey of ["skill", "skillName", "skill_name", "name"]) {
|
|
111
|
+
const rawName = input[inputKey];
|
|
112
|
+
if (typeof rawName !== "string") continue;
|
|
113
|
+
const lower = rawName.trim().toLowerCase();
|
|
114
|
+
const name = selected.get(lower);
|
|
115
|
+
if (!name) continue;
|
|
116
|
+
const key = `tool:${source}:${lower}`;
|
|
117
|
+
evidence.set(key, { key, name });
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return [...evidence.values()];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function normalizeUsageFile(value: unknown): SkillUsageStats {
|
|
125
|
+
const source = value && typeof value === "object" ? value as { skills?: unknown } : {};
|
|
126
|
+
const rawSkills = source.skills && typeof source.skills === "object" && !Array.isArray(source.skills)
|
|
127
|
+
? source.skills as Record<string, unknown>
|
|
128
|
+
: value && typeof value === "object" && !Array.isArray(value)
|
|
129
|
+
? value as Record<string, unknown>
|
|
130
|
+
: {};
|
|
131
|
+
const out: SkillUsageStats = {};
|
|
132
|
+
for (const [name, raw] of Object.entries(rawSkills)) {
|
|
133
|
+
if (!raw || typeof raw !== "object") continue;
|
|
134
|
+
const entry = raw as { count?: unknown; lastUsed?: unknown };
|
|
135
|
+
const count = typeof entry.count === "number" && Number.isFinite(entry.count) && entry.count > 0 ? Math.floor(entry.count) : 0;
|
|
136
|
+
const lastUsed = typeof entry.lastUsed === "number" && Number.isFinite(entry.lastUsed) && entry.lastUsed > 0 ? Math.floor(entry.lastUsed) : 0;
|
|
137
|
+
if (count > 0) out[name] = { count, lastUsed };
|
|
138
|
+
}
|
|
139
|
+
return out;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function toUsageFile(stats: SkillUsageStats, now = Date.now()): SkillUsageFile {
|
|
143
|
+
return { version: 1, updatedAt: new Date(now).toISOString(), skills: stats };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function recordSkillUsage(stats: SkillUsageStats, names: readonly string[], now = Date.now()): SkillUsageStats {
|
|
147
|
+
const next: SkillUsageStats = { ...stats };
|
|
148
|
+
for (const name of new Set(names.filter(Boolean))) {
|
|
149
|
+
const prev = next[name] ?? { count: 0, lastUsed: 0 };
|
|
150
|
+
next[name] = { count: prev.count + 1, lastUsed: now };
|
|
151
|
+
}
|
|
152
|
+
return next;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Merge a process-local increment delta into the latest persisted snapshot. */
|
|
156
|
+
export function mergeUsageStats(base: SkillUsageStats, delta: SkillUsageStats): SkillUsageStats {
|
|
157
|
+
const next: SkillUsageStats = { ...base };
|
|
158
|
+
for (const [name, entry] of Object.entries(delta)) {
|
|
159
|
+
const previous = next[name] ?? { count: 0, lastUsed: 0 };
|
|
160
|
+
next[name] = {
|
|
161
|
+
count: previous.count + entry.count,
|
|
162
|
+
lastUsed: Math.max(previous.lastUsed, entry.lastUsed),
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
return next;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Pick conservative usage signals for future pinning.
|
|
170
|
+
*
|
|
171
|
+
* Do not feed every ranked/selected skill back into usage stats: ranking is a
|
|
172
|
+
* retrieval decision, not proof that the model actually wanted the skill. We
|
|
173
|
+
* record only explicit user mentions or prior skill tool invocations visible in
|
|
174
|
+
* the conversation, filtered to skills that survived the current optimizer pass.
|
|
175
|
+
*/
|
|
176
|
+
export function selectUsageRecordSkills(payload: unknown, selectedSkills: readonly string[]): string[] {
|
|
177
|
+
return [...new Set(collectSkillUsageEvidence(payload, selectedSkills).map((entry) => entry.name))];
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function usageRecordSignature(payload: unknown, names: readonly string[]): string {
|
|
181
|
+
return collectSkillUsageEvidence(payload, names).map((entry) => entry.key).sort().join("\n");
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export const DEFAULT_USAGE_HALF_LIFE_DAYS = 30;
|
|
185
|
+
|
|
186
|
+
export interface UsagePriorOptions {
|
|
187
|
+
now?: number;
|
|
188
|
+
halfLifeDays?: number;
|
|
189
|
+
minimumScore?: number;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Frequency is useful evidence only while it is recent. Exponential decay
|
|
194
|
+
* prevents an old high count from becoming a permanent routing decision.
|
|
195
|
+
*/
|
|
196
|
+
export function scoreSkillUsage(
|
|
197
|
+
entry: SkillUsageEntry,
|
|
198
|
+
now = Date.now(),
|
|
199
|
+
halfLifeDays = DEFAULT_USAGE_HALF_LIFE_DAYS,
|
|
200
|
+
): number {
|
|
201
|
+
if (entry.count <= 0 || entry.lastUsed <= 0) return 0;
|
|
202
|
+
const safeHalfLife = Math.max(1, halfLifeDays);
|
|
203
|
+
const ageDays = Math.max(0, now - entry.lastUsed) / 86_400_000;
|
|
204
|
+
return Math.log1p(entry.count) * 2 ** (-ageDays / safeHalfLife);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export function buildUsagePrior(
|
|
208
|
+
stats: SkillUsageStats,
|
|
209
|
+
options: UsagePriorOptions = {},
|
|
210
|
+
): Record<string, number> {
|
|
211
|
+
const now = options.now ?? Date.now();
|
|
212
|
+
const halfLifeDays = options.halfLifeDays ?? DEFAULT_USAGE_HALF_LIFE_DAYS;
|
|
213
|
+
const minimumScore = Math.max(0, options.minimumScore ?? 1e-6);
|
|
214
|
+
const entries = Object.entries(stats)
|
|
215
|
+
.map(([name, entry]) => [name, scoreSkillUsage(entry, now, halfLifeDays)] as const)
|
|
216
|
+
.filter(([, score]) => score >= minimumScore)
|
|
217
|
+
.sort(([leftName, leftScore], [rightName, rightScore]) => rightScore - leftScore || leftName.localeCompare(rightName));
|
|
218
|
+
return Object.fromEntries(entries);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Conservatively bound usage history. Age removes only one-off unprotected
|
|
223
|
+
* entries. The cap then keeps protected entries plus the strongest remaining
|
|
224
|
+
* entries; protected names may intentionally exceed it.
|
|
225
|
+
*/
|
|
226
|
+
export function pruneUsageStats(stats: SkillUsageStats, options: UsagePruneOptions = {}): SkillUsageStats {
|
|
227
|
+
const maxEntries = Number.isInteger(options.maxEntries) && (options.maxEntries as number) >= 0
|
|
228
|
+
? options.maxEntries as number
|
|
229
|
+
: DEFAULT_USAGE_MAX_ENTRIES;
|
|
230
|
+
const staleDays = typeof options.staleDays === "number" && Number.isFinite(options.staleDays) && options.staleDays >= 0
|
|
231
|
+
? options.staleDays
|
|
232
|
+
: DEFAULT_USAGE_STALE_DAYS;
|
|
233
|
+
const now = typeof options.now === "number" && Number.isFinite(options.now) ? options.now : Date.now();
|
|
234
|
+
const cutoff = staleDays === 0 ? Number.NEGATIVE_INFINITY : now - staleDays * 86_400_000;
|
|
235
|
+
const protectedNames = new Set((options.protectedNames ?? []).map((name) => name.toLowerCase()));
|
|
236
|
+
const source = Object.entries(stats);
|
|
237
|
+
const ageFiltered = source.filter(([name, entry]) => {
|
|
238
|
+
if (protectedNames.has(name.toLowerCase())) return true;
|
|
239
|
+
return !(entry.count <= STALE_LOW_USAGE_MAX_COUNT && entry.lastUsed > 0 && entry.lastUsed <= cutoff);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
let kept = ageFiltered;
|
|
243
|
+
if (maxEntries > 0 && ageFiltered.length > maxEntries) {
|
|
244
|
+
const protectedEntries = ageFiltered.filter(([name]) => protectedNames.has(name.toLowerCase()));
|
|
245
|
+
const candidates = ageFiltered
|
|
246
|
+
.filter(([name]) => !protectedNames.has(name.toLowerCase()))
|
|
247
|
+
.sort((a, b) => b[1].count - a[1].count || b[1].lastUsed - a[1].lastUsed || a[0].localeCompare(b[0]));
|
|
248
|
+
const keepNames = new Set([
|
|
249
|
+
...protectedEntries,
|
|
250
|
+
...candidates.slice(0, Math.max(0, maxEntries - protectedEntries.length)),
|
|
251
|
+
].map(([name]) => name));
|
|
252
|
+
kept = ageFiltered.filter(([name]) => keepNames.has(name));
|
|
253
|
+
}
|
|
254
|
+
if (kept.length === source.length) return stats;
|
|
255
|
+
return Object.fromEntries(kept);
|
|
256
|
+
}
|