@zhushanwen/pi-cache-probe 0.1.3 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/__tests__/fingerprint.test.ts +44 -41
- package/src/fingerprint.ts +3 -3
- package/src/index.ts +2 -2
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
## entry schema v2(数据量精简,长期采集友好)
|
|
12
12
|
|
|
13
13
|
- hash 为 sha256 前 16 hex;**baseline entry** 存全量 9 hash + cwd(约 250B);**normal entry** 只存变化项增量(约 120B);无变化 turn 零写入
|
|
14
|
-
- `seq` = 进程内
|
|
14
|
+
- `seq` = 进程内 before_agent_start 触发计数,随 entry 落盘供人工排序诊断;当前无自动消费方(analyze.py 不读 seq)
|
|
15
15
|
|
|
16
16
|
## 挂载方式(长期采集)
|
|
17
17
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhushanwen/pi-cache-probe",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Zero-impact Pi probe extension — records system prompt / tools fingerprint hashes into session custom entries (only on change) to attribute LLM prompt-cache miss to prefix drift vs server-side eviction.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
@@ -1,62 +1,57 @@
|
|
|
1
1
|
// 测试框架:vitest
|
|
2
2
|
// 运行命令:npx vitest run src/__tests__/fingerprint.test.ts
|
|
3
3
|
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
4
|
+
// 指纹纯函数层测试(经公有面 hashOf / buildProbeEntry / extractSystem / sentToolsOf):
|
|
5
|
+
// hash 稳定性(key 序 / undefined→null / 数组有序)、hash 为 16 hex、
|
|
6
|
+
// buildProbeEntry 的 baseline-全量、normal-增量、无变化-null。
|
|
6
7
|
|
|
7
8
|
import { describe, expect, it } from "vitest";
|
|
8
9
|
|
|
9
10
|
import {
|
|
10
11
|
buildProbeEntry,
|
|
11
|
-
diffFingerprints,
|
|
12
12
|
extractSystem,
|
|
13
|
-
HASH_LEN,
|
|
14
13
|
hashOf,
|
|
15
14
|
sentToolsOf,
|
|
16
|
-
stableStringify,
|
|
17
15
|
type Fingerprints,
|
|
18
16
|
} from "../fingerprint";
|
|
19
17
|
|
|
18
|
+
// hash 为 16 hex 是 entry schema v2 的契约(fingerprint.ts 头注释),硬编码断言
|
|
19
|
+
const HASH_HEX_LEN = 16;
|
|
20
|
+
|
|
20
21
|
const fp = (over: Partial<Fingerprints> = {}): Fingerprints => ({
|
|
21
|
-
spFull: "a".repeat(
|
|
22
|
-
toolsSent: "b".repeat(
|
|
23
|
-
contextFiles: "c".repeat(
|
|
24
|
-
skills: "d".repeat(
|
|
25
|
-
toolsList: "e".repeat(
|
|
26
|
-
toolsReg: "f".repeat(
|
|
27
|
-
append: "0".repeat(
|
|
28
|
-
guidelines: "1".repeat(
|
|
29
|
-
customPrompt: "2".repeat(
|
|
22
|
+
spFull: "a".repeat(HASH_HEX_LEN),
|
|
23
|
+
toolsSent: "b".repeat(HASH_HEX_LEN),
|
|
24
|
+
contextFiles: "c".repeat(HASH_HEX_LEN),
|
|
25
|
+
skills: "d".repeat(HASH_HEX_LEN),
|
|
26
|
+
toolsList: "e".repeat(HASH_HEX_LEN),
|
|
27
|
+
toolsReg: "f".repeat(HASH_HEX_LEN),
|
|
28
|
+
append: "0".repeat(HASH_HEX_LEN),
|
|
29
|
+
guidelines: "1".repeat(HASH_HEX_LEN),
|
|
30
|
+
customPrompt: "2".repeat(HASH_HEX_LEN),
|
|
30
31
|
...over,
|
|
31
32
|
});
|
|
32
33
|
|
|
33
|
-
describe("
|
|
34
|
-
it("
|
|
35
|
-
expect(stableStringify({ a: 1, b: { d: 4, c: 3 } })).toBe(stableStringify({ b: { c: 3, d: 4 }, a: 1 }));
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
it("undefined 归一为 null", () => {
|
|
39
|
-
expect(stableStringify(undefined)).toBe("null");
|
|
40
|
-
expect(stableStringify({ x: undefined })).toBe(stableStringify({ x: null }));
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
it("数组保序、原始值直译", () => {
|
|
44
|
-
expect(stableStringify([2, 1])).not.toBe(stableStringify([1, 2]));
|
|
45
|
-
expect(stableStringify("s")).toBe('"s"');
|
|
46
|
-
expect(stableStringify(3)).toBe("3");
|
|
47
|
-
});
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
describe("hashOf", () => {
|
|
51
|
-
it(`hash 为 ${HASH_LEN} hex 字符且同输入同输出`, () => {
|
|
34
|
+
describe("hashOf(stable stringify 语义经公有面断言)", () => {
|
|
35
|
+
it("hash 为 16 hex 字符且同输入同输出", () => {
|
|
52
36
|
const h = hashOf({ a: 1 });
|
|
53
|
-
expect(h).toMatch(
|
|
37
|
+
expect(h).toMatch(new RegExp(`^[0-9a-f]{${HASH_HEX_LEN}}$`));
|
|
54
38
|
expect(hashOf({ a: 1 })).toBe(h);
|
|
55
39
|
expect(hashOf({ b: 1 })).not.toBe(h);
|
|
56
40
|
});
|
|
57
41
|
|
|
58
|
-
it("key 顺序不影响 hash", () => {
|
|
42
|
+
it("key 顺序不影响 hash(防假变化的核心保证,嵌套对象同)", () => {
|
|
59
43
|
expect(hashOf({ a: 1, b: 2 })).toBe(hashOf({ b: 2, a: 1 }));
|
|
44
|
+
expect(hashOf({ a: 1, b: { d: 4, c: 3 } })).toBe(hashOf({ b: { c: 3, d: 4 }, a: 1 }));
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("undefined 归一为 null(与显式 null 同 hash)", () => {
|
|
48
|
+
expect(hashOf(undefined)).toBe(hashOf(null));
|
|
49
|
+
expect(hashOf({ x: undefined })).toBe(hashOf({ x: null }));
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("数组保序、原始值类型不混淆", () => {
|
|
53
|
+
expect(hashOf([2, 1])).not.toBe(hashOf([1, 2]));
|
|
54
|
+
expect(hashOf("3")).not.toBe(hashOf(3));
|
|
60
55
|
});
|
|
61
56
|
});
|
|
62
57
|
|
|
@@ -101,10 +96,18 @@ describe("sentToolsOf", () => {
|
|
|
101
96
|
});
|
|
102
97
|
});
|
|
103
98
|
|
|
104
|
-
describe("
|
|
105
|
-
it("diff
|
|
106
|
-
|
|
107
|
-
|
|
99
|
+
describe("buildProbeEntry(diff 语义经 changed 数组断言)", () => {
|
|
100
|
+
it("diff 只报变化项(changed 数组 + 增量 h)", () => {
|
|
101
|
+
const e = buildProbeEntry(
|
|
102
|
+
fp({ contextFiles: "x".repeat(HASH_HEX_LEN), skills: "y".repeat(HASH_HEX_LEN) }),
|
|
103
|
+
fp(),
|
|
104
|
+
{ seq: 3, needsBaseline: false, startReason: null, cwd: "/w" },
|
|
105
|
+
);
|
|
106
|
+
expect(e?.changed).toEqual(["contextFiles", "skills"]);
|
|
107
|
+
expect(e?.h).toEqual({
|
|
108
|
+
contextFiles: "x".repeat(HASH_HEX_LEN),
|
|
109
|
+
skills: "y".repeat(HASH_HEX_LEN),
|
|
110
|
+
});
|
|
108
111
|
});
|
|
109
112
|
|
|
110
113
|
it("baseline:全量 hash + changed ['*'] + cwd + startReason", () => {
|
|
@@ -123,13 +126,13 @@ describe("diffFingerprints / buildProbeEntry", () => {
|
|
|
123
126
|
|
|
124
127
|
it("normal:只存变化项(增量),不带 cwd", () => {
|
|
125
128
|
const last = fp();
|
|
126
|
-
const cur = fp({ contextFiles: "x".repeat(
|
|
129
|
+
const cur = fp({ contextFiles: "x".repeat(HASH_HEX_LEN) });
|
|
127
130
|
const e = buildProbeEntry(cur, last, { seq: 3, needsBaseline: false, startReason: null, cwd: "/w" });
|
|
128
131
|
expect(e).toEqual({
|
|
129
132
|
v: 2,
|
|
130
133
|
seq: 3,
|
|
131
134
|
changed: ["contextFiles"],
|
|
132
|
-
h: { contextFiles: "x".repeat(
|
|
135
|
+
h: { contextFiles: "x".repeat(HASH_HEX_LEN) },
|
|
133
136
|
});
|
|
134
137
|
});
|
|
135
138
|
|
package/src/fingerprint.ts
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
import { createHash } from "node:crypto";
|
|
12
12
|
|
|
13
13
|
/** 指纹 hash 长度(hex 字符数)。 */
|
|
14
|
-
|
|
14
|
+
const HASH_LEN = 16;
|
|
15
15
|
|
|
16
16
|
/** entry schema 版本(v2:短 hash + 增量 entry)。 */
|
|
17
17
|
export const SCHEMA_VERSION = 2;
|
|
@@ -46,7 +46,7 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
/** 递归 sort keys 的稳定序列化(undefined 归一为 null,防 key 顺序抖动产生假变化)。 */
|
|
49
|
-
|
|
49
|
+
function stableStringify(value: unknown): string {
|
|
50
50
|
if (value === undefined) return "null";
|
|
51
51
|
if (value === null || typeof value !== "object") return JSON.stringify(value);
|
|
52
52
|
if (Array.isArray(value)) return "[" + value.map(stableStringify).join(",") + "]";
|
|
@@ -89,7 +89,7 @@ export function sentToolsOf(payload: unknown): unknown[] | null {
|
|
|
89
89
|
return Array.isArray(tools) ? tools : null;
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
-
|
|
92
|
+
function diffFingerprints(cur: Fingerprints, last: Fingerprints | null): FingerprintKey[] {
|
|
93
93
|
if (last === null) return [];
|
|
94
94
|
return (Object.keys(cur) as FingerprintKey[]).filter((k) => last[k] !== cur[k]);
|
|
95
95
|
}
|
package/src/index.ts
CHANGED
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
* toolsSent(tools 数组),与 pending 合并对比,变化/基线时 appendEntry
|
|
14
14
|
* - agent_end:turn 内无 provider 请求则丢弃 pending(无请求即无归因价值)
|
|
15
15
|
*
|
|
16
|
-
* seq = 进程内 before_agent_start
|
|
17
|
-
*
|
|
16
|
+
* seq = 进程内 before_agent_start 触发计数,随 entry 落盘供人工排序诊断;
|
|
17
|
+
* 当前无自动消费方(analyze.py 不读 seq)。
|
|
18
18
|
*
|
|
19
19
|
* Fail-safe:handler 全程捕获,异常时写 error entry(缺口可见非静默)并重置为
|
|
20
20
|
* 需基线状态;appendEntry 自身失败仅 stderr 诊断,绝不阻塞请求。
|