pi-harness-runtime 1.1.412 → 1.1.414
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/harness/skills-startup.ts +27 -0
- 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/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/cost-optimizer/package.json +1 -1
- package/packages/dependency-analyzer/package.json +1 -1
- 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/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-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/client.d.ts +10 -0
- package/packages/qdrant-skills/dist/client.d.ts.map +1 -1
- package/packages/qdrant-skills/dist/client.js +20 -1
- package/packages/qdrant-skills/dist/client.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/client.ts +19 -1
- package/packages/qdrant-skills/src/index.ts +1 -0
- package/packages/qdrant-skills/test/collection-info.test.ts +37 -1
- 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/loader.d.ts +40 -7
- package/packages/skills/dist/loader.d.ts.map +1 -1
- package/packages/skills/dist/loader.js +65 -20
- package/packages/skills/dist/loader.js.map +1 -1
- package/packages/skills/dist/vector-schema.d.ts +89 -4
- package/packages/skills/dist/vector-schema.d.ts.map +1 -1
- package/packages/skills/dist/vector-schema.js +62 -4
- package/packages/skills/dist/vector-schema.js.map +1 -1
- package/packages/skills/package.json +1 -1
- package/packages/skills/src/loader.ts +102 -25
- package/packages/skills/src/vector-schema.ts +117 -4
- package/packages/skills/test/remote-skills.test.ts +50 -0
- package/packages/skills/test/vector-schema.test.ts +167 -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/worktree/package.json +1 -1
- package/packages/write-review/package.json +1 -1
- package/scripts/sync-to-qdrant.ts +19 -13
|
@@ -22,6 +22,10 @@ import {
|
|
|
22
22
|
LEGACY_SKILL_COLLECTION_MODEL,
|
|
23
23
|
SKILL_COLLECTION,
|
|
24
24
|
describeSkillSchemaMismatch,
|
|
25
|
+
skillPointPayload,
|
|
26
|
+
skillQdrantPoint,
|
|
27
|
+
stableHash,
|
|
28
|
+
type SkillPointPayload,
|
|
25
29
|
type SkillVectorSchema,
|
|
26
30
|
} from "./vector-schema.js";
|
|
27
31
|
import {
|
|
@@ -39,11 +43,15 @@ import {
|
|
|
39
43
|
// At runtime, packages/ is resolved from the monorepo root or node_modules.
|
|
40
44
|
|
|
41
45
|
export interface QdrantSkillPoint {
|
|
46
|
+
/**
|
|
47
|
+
* The id of the point.
|
|
48
|
+
*
|
|
49
|
+
* It is the hash of the name in the payload, so the point id and the
|
|
50
|
+
* payload name always belong together.
|
|
51
|
+
*/
|
|
42
52
|
id: number;
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
body: string;
|
|
46
|
-
text: string;
|
|
53
|
+
/** The payload of the point. */
|
|
54
|
+
payload: SkillPointPayload;
|
|
47
55
|
}
|
|
48
56
|
|
|
49
57
|
export interface QdrantClientLike {
|
|
@@ -98,6 +106,29 @@ export interface QdrantSkillStats {
|
|
|
98
106
|
* nothing does not have to guess why.
|
|
99
107
|
*/
|
|
100
108
|
schemaMismatch?: string;
|
|
109
|
+
/**
|
|
110
|
+
* The collection of an earlier installation that still holds the skills.
|
|
111
|
+
*
|
|
112
|
+
* The runtime collection is empty (or does not exist yet) while the
|
|
113
|
+
* legacy collection holds the skills of the previous version. They are
|
|
114
|
+
* not lost - they live in a collection with another name, and one with
|
|
115
|
+
* another vector size.
|
|
116
|
+
*/
|
|
117
|
+
legacy?: LegacyCollectionInfo;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* The skills of the legacy collection.
|
|
122
|
+
*/
|
|
123
|
+
export interface LegacyCollectionInfo {
|
|
124
|
+
/** The name of the collection. */
|
|
125
|
+
collection: string;
|
|
126
|
+
/** The number of skill vectors it holds. */
|
|
127
|
+
count: number;
|
|
128
|
+
/** The vector size it was created with. */
|
|
129
|
+
dimensions?: number;
|
|
130
|
+
/** The embedding model it was created with. */
|
|
131
|
+
model?: string;
|
|
101
132
|
}
|
|
102
133
|
|
|
103
134
|
/** The information the Qdrant package can report about a collection. */
|
|
@@ -126,7 +157,7 @@ const RUNTIME_VECTOR_DISTANCE = "Cosine";
|
|
|
126
157
|
export interface SkillVectorPoint {
|
|
127
158
|
id: number;
|
|
128
159
|
vector: number[];
|
|
129
|
-
payload:
|
|
160
|
+
payload: SkillPointPayload;
|
|
130
161
|
}
|
|
131
162
|
|
|
132
163
|
/**
|
|
@@ -433,6 +464,43 @@ export class SkillRegistry {
|
|
|
433
464
|
}
|
|
434
465
|
}
|
|
435
466
|
|
|
467
|
+
/**
|
|
468
|
+
* The skills of the legacy collection.
|
|
469
|
+
*
|
|
470
|
+
* The legacy collection holds 384-dimensional vectors, so it cannot be
|
|
471
|
+
* searched with the vectors of the runtime. Its skills are not lost -
|
|
472
|
+
* they live in a collection with another name.
|
|
473
|
+
*/
|
|
474
|
+
private async _readLegacyCollection(
|
|
475
|
+
client: QdrantClientLike,
|
|
476
|
+
mods: QdrantModules,
|
|
477
|
+
): Promise<LegacyCollectionInfo | undefined> {
|
|
478
|
+
// The legacy collection IS the runtime collection when the user
|
|
479
|
+
// configured it explicitly - then there is no second collection.
|
|
480
|
+
if (this._options.collection === LEGACY_SKILL_COLLECTION) return undefined;
|
|
481
|
+
if (!mods.getCollectionInfo) return undefined;
|
|
482
|
+
|
|
483
|
+
try {
|
|
484
|
+
const info = await mods.getCollectionInfo(client, LEGACY_SKILL_COLLECTION);
|
|
485
|
+
if (!info?.exists) return undefined;
|
|
486
|
+
const count = info.pointsCount ?? 0;
|
|
487
|
+
if (count === 0) return undefined;
|
|
488
|
+
return {
|
|
489
|
+
collection: LEGACY_SKILL_COLLECTION,
|
|
490
|
+
count,
|
|
491
|
+
dimensions: info.vectorsSize,
|
|
492
|
+
model:
|
|
493
|
+
info.vectorsSize === LEGACY_SKILL_COLLECTION_DIMENSIONS
|
|
494
|
+
? LEGACY_SKILL_COLLECTION_MODEL
|
|
495
|
+
: undefined,
|
|
496
|
+
};
|
|
497
|
+
} catch {
|
|
498
|
+
// The legacy collection is a bonus - a failure to read it must not
|
|
499
|
+
// hide the state of the runtime collection.
|
|
500
|
+
return undefined;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
436
504
|
/**
|
|
437
505
|
* The schema the runtime expects of the skill collection.
|
|
438
506
|
*
|
|
@@ -500,7 +568,15 @@ export class SkillRegistry {
|
|
|
500
568
|
};
|
|
501
569
|
}
|
|
502
570
|
if (!info.exists) {
|
|
503
|
-
|
|
571
|
+
// The runtime collection does not exist (yet). An earlier
|
|
572
|
+
// installation may have left the skills in the legacy collection -
|
|
573
|
+
// naming them is more useful than an empty count.
|
|
574
|
+
return {
|
|
575
|
+
state: "missing",
|
|
576
|
+
count: 0,
|
|
577
|
+
collection: config.collection,
|
|
578
|
+
legacy: await this._readLegacyCollection(client, mods),
|
|
579
|
+
};
|
|
504
580
|
}
|
|
505
581
|
|
|
506
582
|
const count = info.pointsCount ?? 0;
|
|
@@ -512,6 +588,12 @@ export class SkillRegistry {
|
|
|
512
588
|
distance: info.vectorsDistance,
|
|
513
589
|
};
|
|
514
590
|
|
|
591
|
+
// An empty runtime collection can still have its skills in the
|
|
592
|
+
// legacy one - the same information, one collection further down.
|
|
593
|
+
if (count === 0) {
|
|
594
|
+
stats.legacy = await this._readLegacyCollection(client, mods);
|
|
595
|
+
}
|
|
596
|
+
|
|
515
597
|
const mismatch = describeSkillSchemaMismatch(
|
|
516
598
|
{ dimensions: info.vectorsSize, distance: info.vectorsDistance },
|
|
517
599
|
this._expectedVectorSchema(mods),
|
|
@@ -539,13 +621,17 @@ export class SkillRegistry {
|
|
|
539
621
|
|
|
540
622
|
/** The point of a skill in the vector index. */
|
|
541
623
|
private _toQdrantPoint(skill: Skill): QdrantSkillPoint {
|
|
542
|
-
|
|
543
|
-
|
|
624
|
+
// The payload and the point id come from the shared schema, so the
|
|
625
|
+
// runtime and `scripts/sync-to-qdrant.ts` store the same point for the
|
|
626
|
+
// same skill. A writer that used its own id would leave two points
|
|
627
|
+
// behind - or overwrite an unrelated one.
|
|
628
|
+
const payload = skillPointPayload({
|
|
544
629
|
name: skill.id,
|
|
630
|
+
title: skill.frontmatter.name,
|
|
545
631
|
description: skill.frontmatter.description,
|
|
546
|
-
body: skill.body
|
|
547
|
-
|
|
548
|
-
|
|
632
|
+
body: skill.body,
|
|
633
|
+
});
|
|
634
|
+
return skillQdrantPoint(payload);
|
|
549
635
|
}
|
|
550
636
|
|
|
551
637
|
/**
|
|
@@ -603,7 +689,7 @@ export class SkillRegistry {
|
|
|
603
689
|
*/
|
|
604
690
|
private _scheduleIndex(skill: Skill): void {
|
|
605
691
|
const point = this._toQdrantPoint(skill);
|
|
606
|
-
const hash =
|
|
692
|
+
const hash = stableHash(point.payload.text);
|
|
607
693
|
if (this._indexedHashes.get(skill.id) === hash) return;
|
|
608
694
|
|
|
609
695
|
this._indexedHashes.set(skill.id, hash);
|
|
@@ -655,12 +741,12 @@ export class SkillRegistry {
|
|
|
655
741
|
while (this._indexQueue.length > 0) {
|
|
656
742
|
const batch = this._indexQueue.splice(0, this._batchSize);
|
|
657
743
|
try {
|
|
658
|
-
const vectors = await indexer.embed(batch.map((item) => item.point.text));
|
|
744
|
+
const vectors = await indexer.embed(batch.map((item) => item.point.payload.text));
|
|
659
745
|
const points: SkillVectorPoint[] = [];
|
|
660
746
|
batch.forEach((item, position) => {
|
|
661
747
|
const vector = vectors[position];
|
|
662
748
|
if (!vector || vector.length === 0) return;
|
|
663
|
-
points.push({ id: item.point.id, vector, payload: item.point });
|
|
749
|
+
points.push({ id: item.point.id, vector, payload: item.point.payload });
|
|
664
750
|
});
|
|
665
751
|
if (points.length > 0) await indexer.upsert(points);
|
|
666
752
|
} catch {
|
|
@@ -675,15 +761,6 @@ export class SkillRegistry {
|
|
|
675
761
|
}
|
|
676
762
|
}
|
|
677
763
|
|
|
678
|
-
/** Simple deterministic ID for Qdrant (no bigints needed). */
|
|
679
|
-
private _hashId(id: string): number {
|
|
680
|
-
let h = 0;
|
|
681
|
-
for (let i = 0; i < id.length; i++) {
|
|
682
|
-
h = (Math.imul(31, h) + id.charCodeAt(i)) | 0;
|
|
683
|
-
}
|
|
684
|
-
return Math.abs(h);
|
|
685
|
-
}
|
|
686
|
-
|
|
687
764
|
/**
|
|
688
765
|
* Register a skill (auto-indexes to Qdrant if configured).
|
|
689
766
|
*/
|
|
@@ -1010,12 +1087,12 @@ export class SkillRegistry {
|
|
|
1010
1087
|
const batch = skills.slice(i, i + this._batchSize);
|
|
1011
1088
|
const points = batch.map((skill) => this._toQdrantPoint(skill));
|
|
1012
1089
|
try {
|
|
1013
|
-
const vectors = await indexer.embed(points.map((point) => point.text));
|
|
1090
|
+
const vectors = await indexer.embed(points.map((point) => point.payload.text));
|
|
1014
1091
|
const payload: SkillVectorPoint[] = [];
|
|
1015
1092
|
points.forEach((point, position) => {
|
|
1016
1093
|
const vector = vectors[position];
|
|
1017
1094
|
if (!vector || vector.length === 0) return;
|
|
1018
|
-
payload.push({ id: point.id, vector, payload: point });
|
|
1095
|
+
payload.push({ id: point.id, vector, payload: point.payload });
|
|
1019
1096
|
});
|
|
1020
1097
|
if (payload.length > 0) await indexer.upsert(payload);
|
|
1021
1098
|
} catch {
|
|
@@ -51,12 +51,125 @@ export interface SkillVectorSchema {
|
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
/**
|
|
54
|
-
*
|
|
55
|
-
* does.
|
|
54
|
+
* The payload of a skill point.
|
|
56
55
|
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
56
|
+
* Both writers of the collection - the runtime registry and
|
|
57
|
+
* `scripts/sync-to-qdrant.ts` - store the SAME payload under the SAME
|
|
58
|
+
* point id. A point is addressed by its id, so a writer that used a
|
|
59
|
+
* different id for the same skill would leave two points behind, and a
|
|
60
|
+
* writer whose id happened to collide with a foreign one would overwrite
|
|
61
|
+
* an unrelated skill.
|
|
59
62
|
*/
|
|
63
|
+
export type SkillPointPayload = {
|
|
64
|
+
/** The name of the skill. The point id is derived from it. */
|
|
65
|
+
name: string;
|
|
66
|
+
/** What the skill does. */
|
|
67
|
+
description: string;
|
|
68
|
+
/** The body of the skill. */
|
|
69
|
+
body: string;
|
|
70
|
+
/** The text the vector was built from. */
|
|
71
|
+
text: string;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* The body of a payload is truncated.
|
|
76
|
+
*
|
|
77
|
+
* A payload is an index, not a file store - the complete skill comes from
|
|
78
|
+
* the skill service.
|
|
79
|
+
*/
|
|
80
|
+
export const SKILL_PAYLOAD_BODY_LIMIT = 2000;
|
|
81
|
+
|
|
82
|
+
/** The text of a payload is truncated as well. */
|
|
83
|
+
export const SKILL_PAYLOAD_TEXT_LIMIT = 1500;
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* A stable hash of a string.
|
|
87
|
+
*
|
|
88
|
+
* The same string always produces the same number, in every process and
|
|
89
|
+
* on every machine - a point id has to survive a restart.
|
|
90
|
+
*
|
|
91
|
+
* Two uses:
|
|
92
|
+
* - the id of a point is the hash of the name its payload carries, so the
|
|
93
|
+
* point id and the payload name always belong together. Two writers
|
|
94
|
+
* that store the same name address the same point.
|
|
95
|
+
* - the hash of the text of a skill says whether its content changed.
|
|
96
|
+
*/
|
|
97
|
+
export function stableHash(value: string): number {
|
|
98
|
+
let hash = 0;
|
|
99
|
+
for (let i = 0; i < value.length; i++) {
|
|
100
|
+
hash = (Math.imul(31, hash) + value.charCodeAt(i)) | 0;
|
|
101
|
+
}
|
|
102
|
+
return Math.abs(hash);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* The text a skill vector is built from.
|
|
107
|
+
*
|
|
108
|
+
* The title is added when it differs from the name: a skill whose
|
|
109
|
+
* directory is named differently from its title carries both.
|
|
110
|
+
*/
|
|
111
|
+
export function skillPointText(input: {
|
|
112
|
+
/** The name the payload carries - the identity of the skill. */
|
|
113
|
+
name: string;
|
|
114
|
+
/** The title of the skill, when it differs from the name. */
|
|
115
|
+
title?: string;
|
|
116
|
+
/** What the skill does. */
|
|
117
|
+
description: string;
|
|
118
|
+
/** The body of the skill. */
|
|
119
|
+
body: string;
|
|
120
|
+
}): string {
|
|
121
|
+
const parts = [input.name];
|
|
122
|
+
if (input.title && input.title !== input.name) parts.push(input.title);
|
|
123
|
+
parts.push(input.description);
|
|
124
|
+
parts.push(input.body.slice(0, SKILL_PAYLOAD_TEXT_LIMIT));
|
|
125
|
+
return parts.join(" ");
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* A point of the skill collection.
|
|
130
|
+
*
|
|
131
|
+
* The id is the hash of the name the payload carries, so the point id and
|
|
132
|
+
* the payload name always belong together: two writers that store the
|
|
133
|
+
* same name address the same point.
|
|
134
|
+
*/
|
|
135
|
+
export interface SkillQdrantPoint {
|
|
136
|
+
id: number;
|
|
137
|
+
payload: SkillPointPayload;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* The point of a skill.
|
|
142
|
+
*
|
|
143
|
+
* The runtime registry and `scripts/sync-to-qdrant.ts` both build their
|
|
144
|
+
* points here, so they cannot drift apart. A writer that used its own id
|
|
145
|
+
* would leave a second point for every skill the other one indexes, and
|
|
146
|
+
* an id that happens to collide with a foreign one would overwrite an
|
|
147
|
+
* unrelated skill.
|
|
148
|
+
*/
|
|
149
|
+
export function skillQdrantPoint(payload: SkillPointPayload): SkillQdrantPoint {
|
|
150
|
+
return { id: stableHash(payload.name), payload };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* The payload of a skill point.
|
|
155
|
+
*/
|
|
156
|
+
export function skillPointPayload(input: {
|
|
157
|
+
/** The name the payload carries - the identity of the skill. */
|
|
158
|
+
name: string;
|
|
159
|
+
/** The title of the skill, when it differs from the name. */
|
|
160
|
+
title?: string;
|
|
161
|
+
/** What the skill does. */
|
|
162
|
+
description: string;
|
|
163
|
+
/** The body of the skill. */
|
|
164
|
+
body: string;
|
|
165
|
+
}): SkillPointPayload {
|
|
166
|
+
return {
|
|
167
|
+
name: input.name,
|
|
168
|
+
description: input.description,
|
|
169
|
+
body: input.body.slice(0, SKILL_PAYLOAD_BODY_LIMIT),
|
|
170
|
+
text: skillPointText(input),
|
|
171
|
+
};
|
|
172
|
+
}
|
|
60
173
|
export function describeSkillSchemaMismatch(
|
|
61
174
|
actual: { dimensions?: number; distance?: string },
|
|
62
175
|
expected: SkillVectorSchema,
|
|
@@ -239,4 +239,54 @@ describe("the state of the vector store", () => {
|
|
|
239
239
|
expect(stats.state).toBe("missing");
|
|
240
240
|
expect(stats.count).toBe(0);
|
|
241
241
|
});
|
|
242
|
+
|
|
243
|
+
it("names the skills of the legacy collection", async () => {
|
|
244
|
+
// The runtime collection does not exist yet, but the collection of
|
|
245
|
+
// the previous version still holds the skills. They are not lost -
|
|
246
|
+
// the reader has to know where they are.
|
|
247
|
+
const client = {
|
|
248
|
+
async upsert(): Promise<void> {},
|
|
249
|
+
async query(): Promise<unknown> {
|
|
250
|
+
return { points: [] };
|
|
251
|
+
},
|
|
252
|
+
async getCollection(name: string): Promise<unknown> {
|
|
253
|
+
if (name === "pi-harness-skills") {
|
|
254
|
+
return {
|
|
255
|
+
points_count: 79,
|
|
256
|
+
config: { params: { vectors: { size: 384, distance: "Cosine" } } },
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
throw new Error("Not Found");
|
|
260
|
+
},
|
|
261
|
+
};
|
|
262
|
+
const registry = createSkillRegistry({
|
|
263
|
+
qdrantClient: client as never,
|
|
264
|
+
embedder: fakeEmbedder,
|
|
265
|
+
collection: "pi-harness-skills-openai-v1",
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
const stats = await registry.getQdrantStats();
|
|
269
|
+
|
|
270
|
+
expect(stats.state).toBe("missing");
|
|
271
|
+
expect(stats.legacy?.collection).toBe("pi-harness-skills");
|
|
272
|
+
expect(stats.legacy?.count).toBe(79);
|
|
273
|
+
expect(stats.legacy?.dimensions).toBe(384);
|
|
274
|
+
expect(stats.legacy?.model).toBe("sentence-transformers/all-MiniLM-L6-v2");
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it("does not look for a second collection when the legacy one is the runtime one", async () => {
|
|
278
|
+
const client = fakeClient([], {
|
|
279
|
+
points_count: 0,
|
|
280
|
+
config: { params: { vectors: { size: 1536, distance: "Cosine" } } },
|
|
281
|
+
});
|
|
282
|
+
const registry = createSkillRegistry({
|
|
283
|
+
qdrantClient: client,
|
|
284
|
+
embedder: fakeEmbedder,
|
|
285
|
+
collection: "pi-harness-skills",
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
const stats = await registry.getQdrantStats();
|
|
289
|
+
|
|
290
|
+
expect(stats.legacy).toBeUndefined();
|
|
291
|
+
});
|
|
242
292
|
});
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regression tests for the point identity of the skill collection.
|
|
3
|
+
*
|
|
4
|
+
* Two writers fill the same collection: the runtime registry and
|
|
5
|
+
* `scripts/sync-to-qdrant.ts`. They used to disagree about the identity of
|
|
6
|
+
* a point - the runtime hashed the name of the skill, the script used a
|
|
7
|
+
* running number. The same skill then existed twice, and a running number
|
|
8
|
+
* could overwrite a point of an unrelated skill.
|
|
9
|
+
*
|
|
10
|
+
* The identity now comes from one shared place, and these tests hold it
|
|
11
|
+
* there.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { describe, expect, test } from "bun:test";
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
createSkillRegistry,
|
|
18
|
+
type SkillVectorIndexer,
|
|
19
|
+
type SkillVectorPoint,
|
|
20
|
+
} from "../src/loader.js";
|
|
21
|
+
import {
|
|
22
|
+
SKILL_PAYLOAD_BODY_LIMIT,
|
|
23
|
+
SKILL_PAYLOAD_TEXT_LIMIT,
|
|
24
|
+
skillPointPayload,
|
|
25
|
+
skillQdrantPoint,
|
|
26
|
+
stableHash,
|
|
27
|
+
} from "../src/vector-schema.js";
|
|
28
|
+
import type { Skill } from "../src/types.js";
|
|
29
|
+
|
|
30
|
+
/** Wait until the registry has no index work left. */
|
|
31
|
+
async function settle(registry: { isIndexing(): boolean }): Promise<void> {
|
|
32
|
+
for (let attempt = 0; attempt < 200 && registry.isIndexing(); attempt++) {
|
|
33
|
+
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** A skill with the given id and body. */
|
|
38
|
+
function makeSkill(id: string, body = `body of ${id}`): Skill {
|
|
39
|
+
return {
|
|
40
|
+
id,
|
|
41
|
+
path: `/tmp/skills/${id}`,
|
|
42
|
+
frontmatter: {
|
|
43
|
+
name: id,
|
|
44
|
+
description: `the ${id} skill`,
|
|
45
|
+
},
|
|
46
|
+
body,
|
|
47
|
+
metadata: {} as Skill["metadata"],
|
|
48
|
+
} as Skill;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** An indexer that keeps the points it receives. */
|
|
52
|
+
class RecordingIndexer implements SkillVectorIndexer {
|
|
53
|
+
readonly upsertCalls: SkillVectorPoint[][] = [];
|
|
54
|
+
|
|
55
|
+
async embed(texts: string[]): Promise<number[][]> {
|
|
56
|
+
return texts.map((text) => [text.length, 1, 2]);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async upsert(points: SkillVectorPoint[]): Promise<void> {
|
|
60
|
+
this.upsertCalls.push(points);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
describe("the identity of a skill point", () => {
|
|
65
|
+
test("the id of the point is the hash of the name in the payload", () => {
|
|
66
|
+
const payload = skillPointPayload({
|
|
67
|
+
name: "a-skill",
|
|
68
|
+
description: "does something",
|
|
69
|
+
body: "the body",
|
|
70
|
+
});
|
|
71
|
+
const point = skillQdrantPoint(payload);
|
|
72
|
+
|
|
73
|
+
expect(point.id).toBe(stableHash(payload.name));
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("the same name always addresses the same point", () => {
|
|
77
|
+
const first = skillQdrantPoint(
|
|
78
|
+
skillPointPayload({ name: "a-skill", description: "one", body: "one" }),
|
|
79
|
+
);
|
|
80
|
+
const second = skillQdrantPoint(
|
|
81
|
+
skillPointPayload({ name: "a-skill", description: "two", body: "two" }),
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
// The content may change, the identity of the point may not.
|
|
85
|
+
expect(second.id).toBe(first.id);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("different names address different points", () => {
|
|
89
|
+
const first = skillQdrantPoint(
|
|
90
|
+
skillPointPayload({ name: "a-skill", description: "d", body: "b" }),
|
|
91
|
+
);
|
|
92
|
+
const second = skillQdrantPoint(
|
|
93
|
+
skillPointPayload({ name: "b-skill", description: "d", body: "b" }),
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
expect(second.id).not.toBe(first.id);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("the payload carries the shared fields and nothing else", () => {
|
|
100
|
+
const payload = skillPointPayload({
|
|
101
|
+
name: "a-skill",
|
|
102
|
+
title: "A Skill",
|
|
103
|
+
description: "does something",
|
|
104
|
+
body: "the body",
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
expect(Object.keys(payload).sort()).toEqual([
|
|
108
|
+
"body",
|
|
109
|
+
"description",
|
|
110
|
+
"name",
|
|
111
|
+
"text",
|
|
112
|
+
]);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("the payload and the text are truncated", () => {
|
|
116
|
+
const payload = skillPointPayload({
|
|
117
|
+
name: "a-skill",
|
|
118
|
+
description: "d",
|
|
119
|
+
body: "x".repeat(SKILL_PAYLOAD_BODY_LIMIT + 500),
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
expect(payload.body.length).toBe(SKILL_PAYLOAD_BODY_LIMIT);
|
|
123
|
+
expect(payload.text.length).toBeLessThanOrEqual(
|
|
124
|
+
SKILL_PAYLOAD_TEXT_LIMIT + payload.name.length + payload.description.length + 10,
|
|
125
|
+
);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test("the title is part of the text when it differs from the name", () => {
|
|
129
|
+
const withTitle = skillPointPayload({
|
|
130
|
+
name: "a-skill",
|
|
131
|
+
title: "A Human Title",
|
|
132
|
+
description: "d",
|
|
133
|
+
body: "b",
|
|
134
|
+
});
|
|
135
|
+
const withoutTitle = skillPointPayload({
|
|
136
|
+
name: "a-skill",
|
|
137
|
+
description: "d",
|
|
138
|
+
body: "b",
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
expect(withTitle.text).toContain("A Human Title");
|
|
142
|
+
expect(withoutTitle.text).not.toContain("A Human Title");
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
describe("the runtime writes the points of the shared schema", () => {
|
|
147
|
+
test("the indexed point carries the shared payload and id", async () => {
|
|
148
|
+
const indexer = new RecordingIndexer();
|
|
149
|
+
const registry = createSkillRegistry({ indexer });
|
|
150
|
+
|
|
151
|
+
registry.register(makeSkill("alpha"));
|
|
152
|
+
await settle(registry);
|
|
153
|
+
|
|
154
|
+
const points = indexer.upsertCalls.flat();
|
|
155
|
+
expect(points.length).toBe(1);
|
|
156
|
+
|
|
157
|
+
const point = points[0];
|
|
158
|
+
expect(point.id).toBe(stableHash(point.payload.name));
|
|
159
|
+
expect(point.payload.name).toBe("alpha");
|
|
160
|
+
expect(Object.keys(point.payload).sort()).toEqual([
|
|
161
|
+
"body",
|
|
162
|
+
"description",
|
|
163
|
+
"name",
|
|
164
|
+
"text",
|
|
165
|
+
]);
|
|
166
|
+
});
|
|
167
|
+
});
|