pi-harness-runtime 1.1.413 → 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/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/package.json +1 -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 +10 -7
- package/packages/skills/dist/loader.d.ts.map +1 -1
- package/packages/skills/dist/loader.js +15 -19
- 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 +27 -24
- package/packages/skills/src/vector-schema.ts +117 -4
- 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
|
@@ -36,12 +36,70 @@ export const LEGACY_SKILL_COLLECTION_MODEL = "sentence-transformers/all-MiniLM-L
|
|
|
36
36
|
/** The vector size of the legacy collection. */
|
|
37
37
|
export const LEGACY_SKILL_COLLECTION_DIMENSIONS = 384;
|
|
38
38
|
/**
|
|
39
|
-
*
|
|
40
|
-
* does.
|
|
39
|
+
* The body of a payload is truncated.
|
|
41
40
|
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
41
|
+
* A payload is an index, not a file store - the complete skill comes from
|
|
42
|
+
* the skill service.
|
|
44
43
|
*/
|
|
44
|
+
export const SKILL_PAYLOAD_BODY_LIMIT = 2000;
|
|
45
|
+
/** The text of a payload is truncated as well. */
|
|
46
|
+
export const SKILL_PAYLOAD_TEXT_LIMIT = 1500;
|
|
47
|
+
/**
|
|
48
|
+
* A stable hash of a string.
|
|
49
|
+
*
|
|
50
|
+
* The same string always produces the same number, in every process and
|
|
51
|
+
* on every machine - a point id has to survive a restart.
|
|
52
|
+
*
|
|
53
|
+
* Two uses:
|
|
54
|
+
* - the id of a point is the hash of the name its payload carries, so the
|
|
55
|
+
* point id and the payload name always belong together. Two writers
|
|
56
|
+
* that store the same name address the same point.
|
|
57
|
+
* - the hash of the text of a skill says whether its content changed.
|
|
58
|
+
*/
|
|
59
|
+
export function stableHash(value) {
|
|
60
|
+
let hash = 0;
|
|
61
|
+
for (let i = 0; i < value.length; i++) {
|
|
62
|
+
hash = (Math.imul(31, hash) + value.charCodeAt(i)) | 0;
|
|
63
|
+
}
|
|
64
|
+
return Math.abs(hash);
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* The text a skill vector is built from.
|
|
68
|
+
*
|
|
69
|
+
* The title is added when it differs from the name: a skill whose
|
|
70
|
+
* directory is named differently from its title carries both.
|
|
71
|
+
*/
|
|
72
|
+
export function skillPointText(input) {
|
|
73
|
+
const parts = [input.name];
|
|
74
|
+
if (input.title && input.title !== input.name)
|
|
75
|
+
parts.push(input.title);
|
|
76
|
+
parts.push(input.description);
|
|
77
|
+
parts.push(input.body.slice(0, SKILL_PAYLOAD_TEXT_LIMIT));
|
|
78
|
+
return parts.join(" ");
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* The point of a skill.
|
|
82
|
+
*
|
|
83
|
+
* The runtime registry and `scripts/sync-to-qdrant.ts` both build their
|
|
84
|
+
* points here, so they cannot drift apart. A writer that used its own id
|
|
85
|
+
* would leave a second point for every skill the other one indexes, and
|
|
86
|
+
* an id that happens to collide with a foreign one would overwrite an
|
|
87
|
+
* unrelated skill.
|
|
88
|
+
*/
|
|
89
|
+
export function skillQdrantPoint(payload) {
|
|
90
|
+
return { id: stableHash(payload.name), payload };
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* The payload of a skill point.
|
|
94
|
+
*/
|
|
95
|
+
export function skillPointPayload(input) {
|
|
96
|
+
return {
|
|
97
|
+
name: input.name,
|
|
98
|
+
description: input.description,
|
|
99
|
+
body: input.body.slice(0, SKILL_PAYLOAD_BODY_LIMIT),
|
|
100
|
+
text: skillPointText(input),
|
|
101
|
+
};
|
|
102
|
+
}
|
|
45
103
|
export function describeSkillSchemaMismatch(actual, expected) {
|
|
46
104
|
if (actual.dimensions !== undefined && actual.dimensions !== expected.dimensions) {
|
|
47
105
|
const model = expected.model ? ` (${expected.model})` : "";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"vector-schema.js","sourceRoot":"","sources":["../src/vector-schema.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH;;;;;GAKG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG,6BAA6B,CAAC;AAE9D;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,uBAAuB,GAAG,mBAAmB,CAAC;AAE3D,0CAA0C;AAC1C,MAAM,CAAC,MAAM,6BAA6B,GAAG,wCAAwC,CAAC;AAEtF,gDAAgD;AAChD,MAAM,CAAC,MAAM,kCAAkC,GAAG,GAAG,CAAC;
|
|
1
|
+
{"version":3,"file":"vector-schema.js","sourceRoot":"","sources":["../src/vector-schema.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH;;;;;GAKG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG,6BAA6B,CAAC;AAE9D;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,uBAAuB,GAAG,mBAAmB,CAAC;AAE3D,0CAA0C;AAC1C,MAAM,CAAC,MAAM,6BAA6B,GAAG,wCAAwC,CAAC;AAEtF,gDAAgD;AAChD,MAAM,CAAC,MAAM,kCAAkC,GAAG,GAAG,CAAC;AAiCtD;;;;;GAKG;AACH,MAAM,CAAC,MAAM,wBAAwB,GAAG,IAAI,CAAC;AAE7C,kDAAkD;AAClD,MAAM,CAAC,MAAM,wBAAwB,GAAG,IAAI,CAAC;AAE7C;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,UAAU,CAAC,KAAa;IACtC,IAAI,IAAI,GAAG,CAAC,CAAC;IACb,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACtC,IAAI,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,EAAE,IAAI,CAAC,GAAG,KAAK,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;IACzD,CAAC;IACD,OAAO,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;AACxB,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,cAAc,CAAC,KAS9B;IACC,MAAM,KAAK,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAC3B,IAAI,KAAK,CAAC,KAAK,IAAI,KAAK,CAAC,KAAK,KAAK,KAAK,CAAC,IAAI;QAAE,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;IACvE,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC;IAC9B,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,wBAAwB,CAAC,CAAC,CAAC;IAC1D,OAAO,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AACzB,CAAC;AAcD;;;;;;;;GAQG;AACH,MAAM,UAAU,gBAAgB,CAAC,OAA0B;IACzD,OAAO,EAAE,EAAE,EAAE,UAAU,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,CAAC;AACnD,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,iBAAiB,CAAC,KASjC;IACC,OAAO;QACL,IAAI,EAAE,KAAK,CAAC,IAAI;QAChB,WAAW,EAAE,KAAK,CAAC,WAAW;QAC9B,IAAI,EAAE,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,wBAAwB,CAAC;QACnD,IAAI,EAAE,cAAc,CAAC,KAAK,CAAC;KAC5B,CAAC;AACJ,CAAC;AACD,MAAM,UAAU,2BAA2B,CAC1C,MAAkD,EAClD,QAA2B;IAE3B,IAAI,MAAM,CAAC,UAAU,KAAK,SAAS,IAAI,MAAM,CAAC,UAAU,KAAK,QAAQ,CAAC,UAAU,EAAE,CAAC;QAClF,MAAM,KAAK,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,QAAQ,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;QAC3D,OAAO,wBAAwB,MAAM,CAAC,UAAU,iDAAiD,QAAQ,CAAC,UAAU,GAAG,KAAK,EAAE,CAAC;IAChI,CAAC;IACD,IAAI,MAAM,CAAC,QAAQ,KAAK,SAAS,IAAI,MAAM,CAAC,QAAQ,KAAK,QAAQ,CAAC,QAAQ,EAAE,CAAC;QAC5E,OAAO,2BAA2B,MAAM,CAAC,QAAQ,kCAAkC,QAAQ,CAAC,QAAQ,EAAE,CAAC;IACxG,CAAC;IACD,OAAO,IAAI,CAAC;AACb,CAAC"}
|
|
@@ -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 {
|
|
@@ -149,7 +157,7 @@ const RUNTIME_VECTOR_DISTANCE = "Cosine";
|
|
|
149
157
|
export interface SkillVectorPoint {
|
|
150
158
|
id: number;
|
|
151
159
|
vector: number[];
|
|
152
|
-
payload:
|
|
160
|
+
payload: SkillPointPayload;
|
|
153
161
|
}
|
|
154
162
|
|
|
155
163
|
/**
|
|
@@ -613,13 +621,17 @@ export class SkillRegistry {
|
|
|
613
621
|
|
|
614
622
|
/** The point of a skill in the vector index. */
|
|
615
623
|
private _toQdrantPoint(skill: Skill): QdrantSkillPoint {
|
|
616
|
-
|
|
617
|
-
|
|
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({
|
|
618
629
|
name: skill.id,
|
|
630
|
+
title: skill.frontmatter.name,
|
|
619
631
|
description: skill.frontmatter.description,
|
|
620
|
-
body: skill.body
|
|
621
|
-
|
|
622
|
-
|
|
632
|
+
body: skill.body,
|
|
633
|
+
});
|
|
634
|
+
return skillQdrantPoint(payload);
|
|
623
635
|
}
|
|
624
636
|
|
|
625
637
|
/**
|
|
@@ -677,7 +689,7 @@ export class SkillRegistry {
|
|
|
677
689
|
*/
|
|
678
690
|
private _scheduleIndex(skill: Skill): void {
|
|
679
691
|
const point = this._toQdrantPoint(skill);
|
|
680
|
-
const hash =
|
|
692
|
+
const hash = stableHash(point.payload.text);
|
|
681
693
|
if (this._indexedHashes.get(skill.id) === hash) return;
|
|
682
694
|
|
|
683
695
|
this._indexedHashes.set(skill.id, hash);
|
|
@@ -729,12 +741,12 @@ export class SkillRegistry {
|
|
|
729
741
|
while (this._indexQueue.length > 0) {
|
|
730
742
|
const batch = this._indexQueue.splice(0, this._batchSize);
|
|
731
743
|
try {
|
|
732
|
-
const vectors = await indexer.embed(batch.map((item) => item.point.text));
|
|
744
|
+
const vectors = await indexer.embed(batch.map((item) => item.point.payload.text));
|
|
733
745
|
const points: SkillVectorPoint[] = [];
|
|
734
746
|
batch.forEach((item, position) => {
|
|
735
747
|
const vector = vectors[position];
|
|
736
748
|
if (!vector || vector.length === 0) return;
|
|
737
|
-
points.push({ id: item.point.id, vector, payload: item.point });
|
|
749
|
+
points.push({ id: item.point.id, vector, payload: item.point.payload });
|
|
738
750
|
});
|
|
739
751
|
if (points.length > 0) await indexer.upsert(points);
|
|
740
752
|
} catch {
|
|
@@ -749,15 +761,6 @@ export class SkillRegistry {
|
|
|
749
761
|
}
|
|
750
762
|
}
|
|
751
763
|
|
|
752
|
-
/** Simple deterministic ID for Qdrant (no bigints needed). */
|
|
753
|
-
private _hashId(id: string): number {
|
|
754
|
-
let h = 0;
|
|
755
|
-
for (let i = 0; i < id.length; i++) {
|
|
756
|
-
h = (Math.imul(31, h) + id.charCodeAt(i)) | 0;
|
|
757
|
-
}
|
|
758
|
-
return Math.abs(h);
|
|
759
|
-
}
|
|
760
|
-
|
|
761
764
|
/**
|
|
762
765
|
* Register a skill (auto-indexes to Qdrant if configured).
|
|
763
766
|
*/
|
|
@@ -1084,12 +1087,12 @@ export class SkillRegistry {
|
|
|
1084
1087
|
const batch = skills.slice(i, i + this._batchSize);
|
|
1085
1088
|
const points = batch.map((skill) => this._toQdrantPoint(skill));
|
|
1086
1089
|
try {
|
|
1087
|
-
const vectors = await indexer.embed(points.map((point) => point.text));
|
|
1090
|
+
const vectors = await indexer.embed(points.map((point) => point.payload.text));
|
|
1088
1091
|
const payload: SkillVectorPoint[] = [];
|
|
1089
1092
|
points.forEach((point, position) => {
|
|
1090
1093
|
const vector = vectors[position];
|
|
1091
1094
|
if (!vector || vector.length === 0) return;
|
|
1092
|
-
payload.push({ id: point.id, vector, payload: point });
|
|
1095
|
+
payload.push({ id: point.id, vector, payload: point.payload });
|
|
1093
1096
|
});
|
|
1094
1097
|
if (payload.length > 0) await indexer.upsert(payload);
|
|
1095
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,
|
|
@@ -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
|
+
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-harness/two-phase-review",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.414",
|
|
4
4
|
"description": "pi-harness-runtime — two-phase implementation and review workflow (queue review signals, gate the phase, scope the diff)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|