pi-harness-runtime 1.1.410 → 1.1.411
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/package.json +1 -1
- package/packages/a2a-adapter/package.json +1 -1
- package/packages/agent-policy/package.json +1 -1
- package/packages/architecture-generator/package.json +1 -1
- package/packages/auth/package.json +1 -1
- package/packages/autonomous-refactor/package.json +1 -1
- package/packages/autonomous-runtime/package.json +1 -1
- package/packages/autonomous-runtime/test/types.test.ts +59 -32
- package/packages/cache-strategy/package.json +1 -1
- package/packages/capability-registry/package.json +1 -1
- package/packages/checkpoint/package.json +1 -1
- package/packages/cli-plugin-sdk/package.json +1 -1
- package/packages/clipboard/package.json +1 -1
- package/packages/clipboard-plugin/package.json +1 -1
- package/packages/code-generation/package.json +1 -1
- package/packages/code-review/package.json +1 -1
- package/packages/codex-adapter/package.json +1 -1
- package/packages/codex-watcher/package.json +1 -1
- package/packages/config-capture/package.json +1 -1
- package/packages/connection-runtime/package.json +1 -1
- package/packages/context-compiler/package.json +1 -1
- package/packages/context-discovery/package.json +1 -1
- package/packages/context-manager/package.json +1 -1
- package/packages/contracts/package.json +1 -1
- package/packages/cookie-sanitizer/package.json +1 -1
- package/packages/cookie-sanitizer/test/sync.test.ts +7 -1
- package/packages/cost-optimizer/package.json +1 -1
- package/packages/dependency-analyzer/dist/src/imports.js +3 -3
- package/packages/dependency-analyzer/dist/src/imports.js.map +1 -1
- package/packages/dependency-analyzer/package.json +1 -1
- package/packages/dependency-analyzer/src/imports.ts +3 -3
- package/packages/dependency-analyzer/test/analyzer.test.ts +1 -0
- package/packages/django-plugin/package.json +1 -1
- package/packages/doc-generator/package.json +1 -1
- package/packages/durable-review/package.json +1 -1
- package/packages/durable-review/test/acceptance.test.ts +24 -2
- package/packages/durable-review/test/smoke.test.ts +85 -47
- package/packages/e2e-tester/package.json +1 -1
- package/packages/evaluation-engine/package.json +1 -1
- package/packages/evaluation-runner/package.json +1 -1
- package/packages/event-bus/package.json +1 -1
- package/packages/event-bus/test/bus.test.ts +16 -4
- package/packages/event-sink/package.json +1 -1
- package/packages/event-store/package.json +1 -1
- package/packages/experience-replay/package.json +1 -1
- package/packages/feedback-collector/package.json +1 -1
- package/packages/file-copy-helper/package.json +1 -1
- package/packages/framework-detector/package.json +1 -1
- package/packages/framework-plugin-sdk/package.json +1 -1
- package/packages/frappe-plugin/package.json +1 -1
- package/packages/generic-web-plugin/package.json +1 -1
- package/packages/health-monitor/package.json +1 -1
- package/packages/intent-analyzer/package.json +1 -1
- package/packages/jev-judge/package.json +1 -1
- package/packages/knowledge-graph/package.json +1 -1
- package/packages/knowledge-retrieval/package.json +1 -1
- package/packages/laravel-plugin/package.json +1 -1
- package/packages/learning-engine/package.json +1 -1
- package/packages/logging/package.json +1 -1
- package/packages/mcp-adapter/package.json +1 -1
- package/packages/memory-engine/package.json +1 -1
- package/packages/milestone-manager/package.json +1 -1
- package/packages/model-registry/package.json +1 -1
- package/packages/nextjs-plugin/package.json +1 -1
- package/packages/notification/package.json +1 -1
- package/packages/observability/package.json +1 -1
- package/packages/okf-indexer/package.json +1 -1
- package/packages/paperclip-integration/package.json +1 -1
- package/packages/performance-optimizer/package.json +1 -1
- package/packages/permission-mode/package.json +1 -1
- package/packages/privilege-broker/package.json +1 -1
- package/packages/project-analyzer/package.json +1 -1
- package/packages/project-bootstrap/package.json +1 -1
- package/packages/projection-engine/package.json +1 -1
- package/packages/prompt-compiler/package.json +1 -1
- package/packages/prompt-versioning/package.json +1 -1
- package/packages/provider-adapter-sdk/package.json +1 -1
- package/packages/provider-router/package.json +1 -1
- package/packages/provider-selector/package.json +1 -1
- package/packages/providers/package.json +1 -1
- package/packages/qdrant-skills/dist/embedder.d.ts +27 -0
- package/packages/qdrant-skills/dist/embedder.d.ts.map +1 -1
- package/packages/qdrant-skills/dist/embedder.js +54 -0
- package/packages/qdrant-skills/dist/embedder.js.map +1 -1
- package/packages/qdrant-skills/dist/index.d.ts +1 -1
- package/packages/qdrant-skills/dist/index.d.ts.map +1 -1
- package/packages/qdrant-skills/dist/index.js +1 -1
- package/packages/qdrant-skills/dist/index.js.map +1 -1
- package/packages/qdrant-skills/package.json +1 -1
- package/packages/qdrant-skills/src/embedder.ts +90 -0
- package/packages/qdrant-skills/src/index.ts +3 -0
- package/packages/qdrant-skills/test/embedder-batch.test.ts +184 -0
- package/packages/quota-manager/package.json +1 -1
- package/packages/rate-limiter/package.json +1 -1
- package/packages/react-vite-plugin/package.json +1 -1
- package/packages/relay/package.json +1 -1
- package/packages/release-manager/package.json +1 -1
- package/packages/requirement-compiler/package.json +1 -1
- package/packages/runtime/package.json +1 -1
- package/packages/scheduler/package.json +1 -1
- package/packages/scheduler-adapter/package.json +1 -1
- package/packages/session/package.json +1 -1
- package/packages/session-api/package.json +1 -1
- package/packages/session-export/package.json +1 -1
- package/packages/shared-context/package.json +1 -1
- package/packages/skill-mcp-client/package.json +1 -1
- package/packages/skill-registry/package.json +1 -1
- package/packages/skills/dist/index.d.ts +5 -0
- package/packages/skills/dist/index.d.ts.map +1 -1
- package/packages/skills/dist/index.js +4 -1
- package/packages/skills/dist/index.js.map +1 -1
- package/packages/skills/dist/loader.d.ts +94 -3
- package/packages/skills/dist/loader.d.ts.map +1 -1
- package/packages/skills/dist/loader.js +192 -25
- package/packages/skills/dist/loader.js.map +1 -1
- package/packages/skills/package.json +1 -1
- package/packages/skills/src/index.ts +12 -3
- package/packages/skills/src/loader.ts +243 -27
- package/packages/skills/test/init-skills.test.ts +107 -111
- package/packages/skills/test/vector-indexing.test.ts +205 -0
- package/packages/sprint-planner/package.json +1 -1
- package/packages/subscription-engine/package.json +1 -1
- package/packages/task-compiler/package.json +1 -1
- package/packages/telegram/package.json +1 -1
- package/packages/tencentdb-memory/package.json +1 -1
- package/packages/tencentdb-sync/package.json +1 -1
- package/packages/test-data-generator/package.json +1 -1
- package/packages/test-generator/package.json +1 -1
- package/packages/todo-bd-sync/package.json +1 -1
- package/packages/token-estimation/package.json +1 -1
- package/packages/token-optimizer/package.json +1 -1
- package/packages/tooling-preference/package.json +1 -1
- package/packages/trajectory/package.json +1 -1
- package/packages/tui/package.json +1 -1
- package/packages/two-phase-review/package.json +1 -1
- package/packages/types/package.json +1 -1
- package/packages/voice-api/package.json +1 -1
- package/packages/webmcp-tester/package.json +1 -1
- package/packages/wiki/package.json +1 -1
- package/packages/workflow-events/package.json +1 -1
- package/packages/workspace-scanner/package.json +1 -1
- package/packages/workspace-scanner/test/scanner.test.ts +4 -3
- package/packages/worktree/package.json +1 -1
- package/packages/write-review/package.json +1 -1
- package/packages/autonomous-runtime/test/autonomous-runtime.test.ts +0 -194
- package/packages/autonomous-runtime/test/inbox.test.ts +0 -80
|
@@ -23,7 +23,7 @@ import { parseSkillFile, scanSkillDirectory } from "./parser.js";
|
|
|
23
23
|
// Loaded dynamically so skills work without Qdrant configured.
|
|
24
24
|
// At runtime, packages/ is resolved from the monorepo root or node_modules.
|
|
25
25
|
|
|
26
|
-
interface QdrantSkillPoint {
|
|
26
|
+
export interface QdrantSkillPoint {
|
|
27
27
|
id: number;
|
|
28
28
|
name: string;
|
|
29
29
|
description: string;
|
|
@@ -43,13 +43,62 @@ interface QdrantClientLike {
|
|
|
43
43
|
|
|
44
44
|
interface QdrantEmbedderLike {
|
|
45
45
|
createEmbedding(text: string, apiKey?: string): Promise<{ embedding: number[]; tokens: number }>;
|
|
46
|
+
/** One request for many texts. Absent in an older package build. */
|
|
47
|
+
createEmbeddingsBatch?: (texts: string[], apiKey?: string) => Promise<{ embeddings: number[][]; tokens: number }>;
|
|
46
48
|
}
|
|
47
49
|
|
|
50
|
+
/**
|
|
51
|
+
* A point of the vector index: the vector and the payload of one skill.
|
|
52
|
+
*/
|
|
53
|
+
export interface SkillVectorPoint {
|
|
54
|
+
id: number;
|
|
55
|
+
vector: number[];
|
|
56
|
+
payload: QdrantSkillPoint;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* The vector index of a registry.
|
|
61
|
+
*
|
|
62
|
+
* The index is an interface so a caller can supply its own implementation
|
|
63
|
+
* (a test uses a counting double) and so the registry never has to know
|
|
64
|
+
* whether the index lives in Qdrant.
|
|
65
|
+
*/
|
|
66
|
+
export interface SkillVectorIndexer {
|
|
67
|
+
/** One vector per text, in the order of the texts. */
|
|
68
|
+
embed(texts: string[]): Promise<number[][]>;
|
|
69
|
+
/** Store the points of the given skills. */
|
|
70
|
+
upsert(points: SkillVectorPoint[]): Promise<void>;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* The options of a skill registry.
|
|
75
|
+
*/
|
|
76
|
+
export interface SkillRegistryOptions {
|
|
77
|
+
/**
|
|
78
|
+
* The vector index.
|
|
79
|
+
*
|
|
80
|
+
* `undefined` (the default) indexes into the configured Qdrant cluster.
|
|
81
|
+
* `null` disables the vector index entirely. An object is used as the index.
|
|
82
|
+
*/
|
|
83
|
+
indexer?: SkillVectorIndexer | null;
|
|
84
|
+
/**
|
|
85
|
+
* How many skills one indexing request carries. A larger batch costs fewer
|
|
86
|
+
* requests but holds more of them in memory.
|
|
87
|
+
*/
|
|
88
|
+
batchSize?: number;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* The default number of skills one indexing request carries.
|
|
93
|
+
*/
|
|
94
|
+
const DEFAULT_INDEX_BATCH_SIZE = 32;
|
|
95
|
+
|
|
48
96
|
/** Resolve the Qdrant skill module path (monorepo sibling or npm package) */
|
|
49
97
|
async function resolveQdrantModules(): Promise<{
|
|
50
98
|
createQdrantClient: (cfg: unknown) => { upsert(c: string, o: unknown): Promise<void>; query(c: string, o: unknown): Promise<unknown>; };
|
|
51
99
|
createCollection: (client: unknown, name: string, dims: number) => Promise<void>;
|
|
52
100
|
createEmbedding: (text: string, apiKey?: string) => Promise<{ embedding: number[]; tokens: number }>;
|
|
101
|
+
createEmbeddingsBatch?: (texts: string[], apiKey?: string) => Promise<{ embeddings: number[][]; tokens: number }>;
|
|
53
102
|
} | null> {
|
|
54
103
|
// Try the monorepo sibling first (the compiled package, so the module
|
|
55
104
|
// comes from the same build the app ships).
|
|
@@ -60,6 +109,7 @@ async function resolveQdrantModules(): Promise<{
|
|
|
60
109
|
createQdrantClient: unknown;
|
|
61
110
|
createCollection: unknown;
|
|
62
111
|
createEmbedding: unknown;
|
|
112
|
+
createEmbeddingsBatch?: unknown;
|
|
63
113
|
};
|
|
64
114
|
return mod as Awaited<ReturnType<typeof resolveQdrantModules>>;
|
|
65
115
|
} catch {
|
|
@@ -71,11 +121,21 @@ async function resolveQdrantModules(): Promise<{
|
|
|
71
121
|
// The specifier is a variable on purpose: the package is optional and
|
|
72
122
|
// must not be resolved by the type checker.
|
|
73
123
|
const specifier = "@pi-harness/qdrant-skills";
|
|
74
|
-
const mod = (await import(/* @vite-ignore */ specifier)) as {
|
|
124
|
+
const mod = (await import(/* @vite-ignore */ specifier)) as {
|
|
125
|
+
createQdrantClient: unknown;
|
|
126
|
+
createCollection: unknown;
|
|
127
|
+
createEmbedding: unknown;
|
|
128
|
+
createEmbeddingsBatch?: unknown;
|
|
129
|
+
};
|
|
75
130
|
return {
|
|
76
131
|
createQdrantClient: (cfg: unknown) => (mod.createQdrantClient as Function)(cfg) as { upsert(c: string, o: unknown): Promise<void>; query(c: string, o: unknown): Promise<unknown> },
|
|
77
132
|
createCollection: mod.createCollection as (client: unknown, name: string, dims: number) => Promise<void>,
|
|
78
133
|
createEmbedding: mod.createEmbedding as (text: string, apiKey?: string) => Promise<{ embedding: number[]; tokens: number }>,
|
|
134
|
+
// Absent in an older package build: the registry falls back to one
|
|
135
|
+
// request per text, still with a bounded concurrency.
|
|
136
|
+
createEmbeddingsBatch: typeof mod.createEmbeddingsBatch === "function"
|
|
137
|
+
? (mod.createEmbeddingsBatch as (texts: string[], apiKey?: string) => Promise<{ embeddings: number[][]; tokens: number }>)
|
|
138
|
+
: undefined,
|
|
79
139
|
};
|
|
80
140
|
} catch {
|
|
81
141
|
return null;
|
|
@@ -128,6 +188,16 @@ export class SkillRegistry {
|
|
|
128
188
|
private index: SkillIndex = { entries: [], loadedAt: 0 };
|
|
129
189
|
private loading: Map<string, Promise<Skill>> = new Map();
|
|
130
190
|
|
|
191
|
+
// The vector index. `undefined` means "use Qdrant", `null` means "no index".
|
|
192
|
+
private _indexer: SkillVectorIndexer | null | undefined;
|
|
193
|
+
private _batchSize: number;
|
|
194
|
+
// The skills that still have to be indexed, in registration order.
|
|
195
|
+
private _indexQueue: Array<{ skill: Skill; point: QdrantSkillPoint; hash: number }> = [];
|
|
196
|
+
private _indexFlushRunning = false;
|
|
197
|
+
private _indexFlushScheduled = false;
|
|
198
|
+
// The content hash of the version of a skill that was queued or indexed.
|
|
199
|
+
private _indexedHashes: Map<string, number> = new Map();
|
|
200
|
+
|
|
131
201
|
// Qdrant state (lazily initialized on first register if configured)
|
|
132
202
|
private _qdrant: {
|
|
133
203
|
client: QdrantClientLike;
|
|
@@ -137,6 +207,12 @@ export class SkillRegistry {
|
|
|
137
207
|
} | null = null;
|
|
138
208
|
private _qdrantInitPromise: Promise<void> | null = null;
|
|
139
209
|
private _qdrantInitFailed = false;
|
|
210
|
+
private _qdrantIndexer: SkillVectorIndexer | null = null;
|
|
211
|
+
|
|
212
|
+
constructor(options: SkillRegistryOptions = {}) {
|
|
213
|
+
this._indexer = options.indexer;
|
|
214
|
+
this._batchSize = options.batchSize ?? DEFAULT_INDEX_BATCH_SIZE;
|
|
215
|
+
}
|
|
140
216
|
|
|
141
217
|
/**
|
|
142
218
|
* Check if Qdrant initialization has failed (useful for diagnostics).
|
|
@@ -145,6 +221,13 @@ export class SkillRegistry {
|
|
|
145
221
|
return this._qdrantInitFailed;
|
|
146
222
|
}
|
|
147
223
|
|
|
224
|
+
/**
|
|
225
|
+
* Whether skills are still queued for the vector index.
|
|
226
|
+
*/
|
|
227
|
+
isIndexing(): boolean {
|
|
228
|
+
return this._indexFlushRunning || this._indexQueue.length > 0;
|
|
229
|
+
}
|
|
230
|
+
|
|
148
231
|
/**
|
|
149
232
|
* Initialize Qdrant client if configured.
|
|
150
233
|
* Uses a shared init promise to prevent race conditions when multiple
|
|
@@ -181,7 +264,10 @@ export class SkillRegistry {
|
|
|
181
264
|
await mods.createCollection(client, config.collection, 1536);
|
|
182
265
|
this._qdrant = {
|
|
183
266
|
client,
|
|
184
|
-
embedder: {
|
|
267
|
+
embedder: {
|
|
268
|
+
createEmbedding: mods.createEmbedding,
|
|
269
|
+
createEmbeddingsBatch: mods.createEmbeddingsBatch,
|
|
270
|
+
},
|
|
185
271
|
collection: config.collection,
|
|
186
272
|
ready: true,
|
|
187
273
|
};
|
|
@@ -191,28 +277,141 @@ export class SkillRegistry {
|
|
|
191
277
|
}
|
|
192
278
|
}
|
|
193
279
|
|
|
194
|
-
/**
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
private async _indexSkillToQdrant(skill: Skill): Promise<void> {
|
|
198
|
-
if (!this._qdrant?.ready) return;
|
|
199
|
-
|
|
200
|
-
const point: QdrantSkillPoint = {
|
|
280
|
+
/** The point of a skill in the vector index. */
|
|
281
|
+
private _toQdrantPoint(skill: Skill): QdrantSkillPoint {
|
|
282
|
+
return {
|
|
201
283
|
id: this._hashId(skill.id),
|
|
202
284
|
name: skill.id,
|
|
203
285
|
description: skill.frontmatter.description,
|
|
204
286
|
body: skill.body.slice(0, 2000),
|
|
205
287
|
text: [skill.id, skill.frontmatter.name, skill.frontmatter.description, skill.body.slice(0, 1500)].join(" "),
|
|
206
288
|
};
|
|
289
|
+
}
|
|
207
290
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
291
|
+
/**
|
|
292
|
+
* The indexer of the registry: the injected one, or the Qdrant cluster.
|
|
293
|
+
*/
|
|
294
|
+
private _getIndexer(): SkillVectorIndexer | null {
|
|
295
|
+
if (this._indexer !== undefined) return this._indexer;
|
|
296
|
+
if (!this._qdrantIndexer) {
|
|
297
|
+
this._qdrantIndexer = this._createQdrantIndexer();
|
|
298
|
+
}
|
|
299
|
+
return this._qdrantIndexer;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* The Qdrant cluster as an indexer.
|
|
304
|
+
*
|
|
305
|
+
* The batch call of the embeddings endpoint carries many texts in one
|
|
306
|
+
* request. One request per skill exhausted the sockets of the process when
|
|
307
|
+
* a registry held hundreds of skills, and every other request of the
|
|
308
|
+
* program then starved.
|
|
309
|
+
*/
|
|
310
|
+
private _createQdrantIndexer(): SkillVectorIndexer | null {
|
|
311
|
+
const qdrant = this._qdrant;
|
|
312
|
+
if (!qdrant?.ready) return null;
|
|
313
|
+
|
|
314
|
+
return {
|
|
315
|
+
embed: async (texts: string[]): Promise<number[][]> => {
|
|
316
|
+
if (texts.length === 0) return [];
|
|
317
|
+
const batch = qdrant.embedder.createEmbeddingsBatch;
|
|
318
|
+
if (batch) {
|
|
319
|
+
const result = await batch(texts);
|
|
320
|
+
return result.embeddings;
|
|
321
|
+
}
|
|
322
|
+
// An older build without the batch call: one request per text, but
|
|
323
|
+
// the caller keeps the concurrency bounded all the same.
|
|
324
|
+
const embeddings: number[][] = [];
|
|
325
|
+
for (const text of texts) {
|
|
326
|
+
const { embedding } = await qdrant.embedder.createEmbedding(text);
|
|
327
|
+
embeddings.push(embedding);
|
|
328
|
+
}
|
|
329
|
+
return embeddings;
|
|
330
|
+
},
|
|
331
|
+
upsert: async (points: SkillVectorPoint[]): Promise<void> => {
|
|
332
|
+
await qdrant.client.upsert(qdrant.collection, { wait: false, points });
|
|
333
|
+
},
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Queue a skill for the vector index.
|
|
339
|
+
*
|
|
340
|
+
* A skill whose text did not change is not queued again: the registry is
|
|
341
|
+
* filled on every scan, and re-embedding the same skill would pay for the
|
|
342
|
+
* same vector over and over.
|
|
343
|
+
*/
|
|
344
|
+
private _scheduleIndex(skill: Skill): void {
|
|
345
|
+
const point = this._toQdrantPoint(skill);
|
|
346
|
+
const hash = this._hashId(point.text);
|
|
347
|
+
if (this._indexedHashes.get(skill.id) === hash) return;
|
|
348
|
+
|
|
349
|
+
this._indexedHashes.set(skill.id, hash);
|
|
350
|
+
this._indexQueue.push({ skill, point, hash });
|
|
351
|
+
this._scheduleIndexFlush();
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Start the index flush, a microtask from now.
|
|
356
|
+
*
|
|
357
|
+
* A scan registers every skill in one go. Waiting for the microtask lets
|
|
358
|
+
* that burst share one request instead of starting one request per skill.
|
|
359
|
+
*/
|
|
360
|
+
private _scheduleIndexFlush(): void {
|
|
361
|
+
if (this._indexFlushScheduled) return;
|
|
362
|
+
this._indexFlushScheduled = true;
|
|
363
|
+
queueMicrotask(() => {
|
|
364
|
+
this._indexFlushScheduled = false;
|
|
365
|
+
this._runIndexFlush();
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Index the queued skills, one batch at a time.
|
|
371
|
+
*
|
|
372
|
+
* Only one batch is in flight, so the number of open connections stays
|
|
373
|
+
* bound no matter how many skills the registry holds.
|
|
374
|
+
*/
|
|
375
|
+
private _runIndexFlush(): void {
|
|
376
|
+
if (this._indexFlushRunning) return;
|
|
377
|
+
this._indexFlushRunning = true;
|
|
378
|
+
|
|
379
|
+
void this._flushIndexQueue().finally(() => {
|
|
380
|
+
this._indexFlushRunning = false;
|
|
381
|
+
// Skills may have arrived while the flush was running.
|
|
382
|
+
if (this._indexQueue.length > 0) this._scheduleIndexFlush();
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
private async _flushIndexQueue(): Promise<void> {
|
|
387
|
+
if (this._indexer === undefined) await this._ensureQdrant();
|
|
388
|
+
const indexer = this._getIndexer();
|
|
389
|
+
if (!indexer) {
|
|
390
|
+
// No vector index: the skills stay usable without vectors.
|
|
391
|
+
this._indexQueue = [];
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
while (this._indexQueue.length > 0) {
|
|
396
|
+
const batch = this._indexQueue.splice(0, this._batchSize);
|
|
397
|
+
try {
|
|
398
|
+
const vectors = await indexer.embed(batch.map((item) => item.point.text));
|
|
399
|
+
const points: SkillVectorPoint[] = [];
|
|
400
|
+
batch.forEach((item, position) => {
|
|
401
|
+
const vector = vectors[position];
|
|
402
|
+
if (!vector || vector.length === 0) return;
|
|
403
|
+
points.push({ id: item.point.id, vector, payload: item.point });
|
|
404
|
+
});
|
|
405
|
+
if (points.length > 0) await indexer.upsert(points);
|
|
406
|
+
} catch {
|
|
407
|
+
// The vector index is optional. A failed batch may be retried by a
|
|
408
|
+
// later registration of the same skills.
|
|
409
|
+
for (const item of batch) {
|
|
410
|
+
if (this._indexedHashes.get(item.skill.id) === item.hash) {
|
|
411
|
+
this._indexedHashes.delete(item.skill.id);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
216
415
|
}
|
|
217
416
|
}
|
|
218
417
|
|
|
@@ -231,8 +430,8 @@ export class SkillRegistry {
|
|
|
231
430
|
register(skill: Skill): void {
|
|
232
431
|
this.skills.set(skill.id, skill);
|
|
233
432
|
this.updateIndex(skill);
|
|
234
|
-
//
|
|
235
|
-
this.
|
|
433
|
+
// Queue the skill for the vector index without blocking registration.
|
|
434
|
+
this._scheduleIndex(skill);
|
|
236
435
|
}
|
|
237
436
|
|
|
238
437
|
/**
|
|
@@ -413,13 +612,30 @@ export class SkillRegistry {
|
|
|
413
612
|
* Useful after Qdrant becomes available or collection is rebuilt.
|
|
414
613
|
*/
|
|
415
614
|
async syncAllToQdrant(onProgress?: (index: number, total: number) => void): Promise<void> {
|
|
416
|
-
await this._ensureQdrant();
|
|
417
|
-
|
|
615
|
+
if (this._indexer === undefined) await this._ensureQdrant();
|
|
616
|
+
const indexer = this._getIndexer();
|
|
617
|
+
if (!indexer) return;
|
|
418
618
|
|
|
419
619
|
const skills = this.list();
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
620
|
+
const total = skills.length;
|
|
621
|
+
let done = 0;
|
|
622
|
+
for (let i = 0; i < total; i += this._batchSize) {
|
|
623
|
+
const batch = skills.slice(i, i + this._batchSize);
|
|
624
|
+
const points = batch.map((skill) => this._toQdrantPoint(skill));
|
|
625
|
+
try {
|
|
626
|
+
const vectors = await indexer.embed(points.map((point) => point.text));
|
|
627
|
+
const payload: SkillVectorPoint[] = [];
|
|
628
|
+
points.forEach((point, position) => {
|
|
629
|
+
const vector = vectors[position];
|
|
630
|
+
if (!vector || vector.length === 0) return;
|
|
631
|
+
payload.push({ id: point.id, vector, payload: point });
|
|
632
|
+
});
|
|
633
|
+
if (payload.length > 0) await indexer.upsert(payload);
|
|
634
|
+
} catch {
|
|
635
|
+
// The vector index is optional — a failed batch does not stop the sync.
|
|
636
|
+
}
|
|
637
|
+
done += batch.length;
|
|
638
|
+
onProgress?.(done, total);
|
|
423
639
|
}
|
|
424
640
|
}
|
|
425
641
|
|
|
@@ -576,8 +792,8 @@ export class SkillRegistry {
|
|
|
576
792
|
/**
|
|
577
793
|
* Create a new skill registry
|
|
578
794
|
*/
|
|
579
|
-
export function createSkillRegistry(): SkillRegistry {
|
|
580
|
-
return new SkillRegistry();
|
|
795
|
+
export function createSkillRegistry(options: SkillRegistryOptions = {}): SkillRegistry {
|
|
796
|
+
return new SkillRegistry(options);
|
|
581
797
|
}
|
|
582
798
|
|
|
583
799
|
/**
|
|
@@ -1,118 +1,114 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* Usage:
|
|
13
|
-
* bun packages/skills/test/init-skills.test.ts
|
|
2
|
+
* The initialization of the skills system.
|
|
3
|
+
*
|
|
4
|
+
* The test used to be a script that ran `initSkills()` while the module was
|
|
5
|
+
* imported. That scanned the real skill directories of the machine and
|
|
6
|
+
* started the vector indexing of every skill it found, which opened a
|
|
7
|
+
* connection per skill and starved every other test of the process.
|
|
8
|
+
*
|
|
9
|
+
* The test now uses a temporary directory and a registry without a vector
|
|
10
|
+
* index, so it neither reads the machine nor reaches the network.
|
|
14
11
|
*/
|
|
15
12
|
|
|
13
|
+
import { describe, it, expect, beforeEach, afterEach } from "bun:test";
|
|
14
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
15
|
+
import { tmpdir } from "node:os";
|
|
16
|
+
import { join } from "node:path";
|
|
17
|
+
|
|
16
18
|
import { initSkills } from "../src/index.js";
|
|
19
|
+
import { createSkillRegistry } from "../src/loader.js";
|
|
20
|
+
|
|
21
|
+
let tempDir: string;
|
|
17
22
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// Test 2: explicit locations
|
|
39
|
-
console.log("[Test 2] initSkills({ locations: [...] })...");
|
|
40
|
-
try {
|
|
41
|
-
const result = initSkills({ locations: ["/tmp/test"] });
|
|
42
|
-
if (!result) {
|
|
43
|
-
throw new Error("No result returned");
|
|
44
|
-
}
|
|
45
|
-
console.log(` PASS: Returned ${result.skills.length} skills\n`);
|
|
46
|
-
passed++;
|
|
47
|
-
} catch (error) {
|
|
48
|
-
console.error(` FAIL: ${error}\n`);
|
|
49
|
-
failed++;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// Test 3: undefined locations (the bug case)
|
|
53
|
-
console.log("[Test 3] initSkills({ locations: undefined }) - THE BUG CASE...");
|
|
54
|
-
try {
|
|
55
|
-
const result = initSkills({ locations: undefined });
|
|
56
|
-
if (!result || !Array.isArray(result.skills)) {
|
|
57
|
-
throw new Error("Invalid result structure");
|
|
58
|
-
}
|
|
59
|
-
console.log(` PASS: Returned ${result.skills.length} skills\n`);
|
|
60
|
-
passed++;
|
|
61
|
-
} catch (error) {
|
|
62
|
-
console.error(` FAIL: ${error}\n`);
|
|
63
|
-
failed++;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// Test 4: partial config with only maxDepth
|
|
67
|
-
console.log("[Test 4] initSkills({ maxDepth: 5 })...");
|
|
68
|
-
try {
|
|
69
|
-
const result = initSkills({ maxDepth: 5 });
|
|
70
|
-
if (!result || !Array.isArray(result.skills)) {
|
|
71
|
-
throw new Error("Invalid result structure");
|
|
72
|
-
}
|
|
73
|
-
console.log(` PASS: Returned ${result.skills.length} skills\n`);
|
|
74
|
-
passed++;
|
|
75
|
-
} catch (error) {
|
|
76
|
-
console.error(` FAIL: ${error}\n`);
|
|
77
|
-
failed++;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// Test 5: partial config with only includeHidden
|
|
81
|
-
console.log("[Test 5] initSkills({ includeHidden: true })...");
|
|
82
|
-
try {
|
|
83
|
-
const result = initSkills({ includeHidden: true });
|
|
84
|
-
if (!result || !Array.isArray(result.skills)) {
|
|
85
|
-
throw new Error("Invalid result structure");
|
|
86
|
-
}
|
|
87
|
-
console.log(` PASS: Returned ${result.skills.length} skills\n`);
|
|
88
|
-
passed++;
|
|
89
|
-
} catch (error) {
|
|
90
|
-
console.error(` FAIL: ${error}\n`);
|
|
91
|
-
failed++;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// Test 6: empty locations array
|
|
95
|
-
console.log("[Test 6] initSkills({ locations: [] })...");
|
|
96
|
-
try {
|
|
97
|
-
const result = initSkills({ locations: [] });
|
|
98
|
-
if (!result) {
|
|
99
|
-
throw new Error("No result returned");
|
|
100
|
-
}
|
|
101
|
-
console.log(` PASS: Returned ${result.skills.length} skills\n`);
|
|
102
|
-
passed++;
|
|
103
|
-
} catch (error) {
|
|
104
|
-
console.error(` FAIL: ${error}\n`);
|
|
105
|
-
failed++;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// Summary
|
|
109
|
-
console.log("=== Summary ===");
|
|
110
|
-
console.log(`Passed: ${passed}`);
|
|
111
|
-
console.log(`Failed: ${failed}`);
|
|
112
|
-
|
|
113
|
-
if (failed > 0) {
|
|
114
|
-
process.exit(1);
|
|
115
|
-
}
|
|
23
|
+
/** Write a skill into a directory of the temporary home. */
|
|
24
|
+
function writeSkill(directory: string, name: string): void {
|
|
25
|
+
const skillDir = join(tempDir, directory, name);
|
|
26
|
+
mkdirSync(skillDir, { recursive: true });
|
|
27
|
+
writeFileSync(
|
|
28
|
+
join(skillDir, "SKILL.md"),
|
|
29
|
+
[
|
|
30
|
+
"---",
|
|
31
|
+
`name: ${name}`,
|
|
32
|
+
`description: The ${name} skill.`,
|
|
33
|
+
"version: 1.0.0",
|
|
34
|
+
"---",
|
|
35
|
+
"",
|
|
36
|
+
`The body of the ${name} skill.`,
|
|
37
|
+
"",
|
|
38
|
+
].join("\n"),
|
|
39
|
+
"utf8",
|
|
40
|
+
);
|
|
116
41
|
}
|
|
117
42
|
|
|
118
|
-
|
|
43
|
+
beforeEach(() => {
|
|
44
|
+
tempDir = mkdtempSync(join(tmpdir(), "test-init-skills-"));
|
|
45
|
+
mkdirSync(join(tempDir, "skills"), { recursive: true });
|
|
46
|
+
writeSkill("skills", "alpha");
|
|
47
|
+
writeSkill("skills", "beta");
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
afterEach(() => {
|
|
51
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe("the initialization of the skills system", () => {
|
|
55
|
+
it("scans the given locations", () => {
|
|
56
|
+
const result = initSkills({
|
|
57
|
+
locations: [join(tempDir, "skills")],
|
|
58
|
+
registry: createSkillRegistry({ indexer: null }),
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
expect(result.skills.length).toBe(2);
|
|
62
|
+
expect(result.skills.map((skill) => skill.id).sort()).toEqual(["alpha", "beta"]);
|
|
63
|
+
expect(result.errors).toEqual([]);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("falls back to the default locations when none are given", () => {
|
|
67
|
+
// The default locations point at the machine of the test, so the
|
|
68
|
+
// result is only checked for its shape.
|
|
69
|
+
const result = initSkills({
|
|
70
|
+
registry: createSkillRegistry({ indexer: null }),
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
expect(Array.isArray(result.skills)).toBe(true);
|
|
74
|
+
expect(Array.isArray(result.errors)).toBe(true);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("falls back to the default locations when they are undefined", () => {
|
|
78
|
+
// The bug case: an undefined value must not override the defaults.
|
|
79
|
+
const result = initSkills({
|
|
80
|
+
locations: undefined,
|
|
81
|
+
registry: createSkillRegistry({ indexer: null }),
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
expect(Array.isArray(result.skills)).toBe(true);
|
|
85
|
+
expect(Array.isArray(result.errors)).toBe(true);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("accepts a partial configuration", () => {
|
|
89
|
+
const result = initSkills({
|
|
90
|
+
maxDepth: 5,
|
|
91
|
+
includeHidden: true,
|
|
92
|
+
registry: createSkillRegistry({ indexer: null }),
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
expect(Array.isArray(result.skills)).toBe(true);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("accepts an empty list of locations", () => {
|
|
99
|
+
const result = initSkills({
|
|
100
|
+
locations: [],
|
|
101
|
+
registry: createSkillRegistry({ indexer: null }),
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
expect(result.skills).toEqual([]);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("registers the scanned skills in the registry", () => {
|
|
108
|
+
const registry = createSkillRegistry({ indexer: null });
|
|
109
|
+
|
|
110
|
+
initSkills({ locations: [join(tempDir, "skills")], registry });
|
|
111
|
+
|
|
112
|
+
expect(registry.list().length).toBe(2);
|
|
113
|
+
});
|
|
114
|
+
});
|