@zhushanwen/pi-cache-probe 0.1.3 → 0.3.1
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 +4 -1
- package/src/__tests__/fingerprint.test.ts +45 -42
- package/src/fingerprint.ts +11 -8
- package/src/index.ts +11 -8
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.1
|
|
3
|
+
"version": "0.3.1",
|
|
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",
|
|
@@ -25,6 +25,9 @@
|
|
|
25
25
|
"index.ts",
|
|
26
26
|
"analyze.py"
|
|
27
27
|
],
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@zhushanwen/pi-ext-guards": "0.4.0"
|
|
30
|
+
},
|
|
28
31
|
"devDependencies": {
|
|
29
32
|
"@vitest/coverage-v8": "^4.1.9",
|
|
30
33
|
"vitest": "^4.1.8"
|
|
@@ -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
|
|
|
@@ -79,7 +74,7 @@ describe("extractSystem(payload 四口径)", () => {
|
|
|
79
74
|
]);
|
|
80
75
|
});
|
|
81
76
|
|
|
82
|
-
it("
|
|
77
|
+
it("伪造对象形态:顶层 systemInstruction(实装恒传 string,仅测试构造可达)", () => {
|
|
83
78
|
expect(extractSystem({ systemInstruction: { role: "system", parts: [] } })).toEqual({
|
|
84
79
|
role: "system",
|
|
85
80
|
parts: [],
|
|
@@ -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
|
@@ -10,8 +10,10 @@
|
|
|
10
10
|
|
|
11
11
|
import { createHash } from "node:crypto";
|
|
12
12
|
|
|
13
|
+
import { isRecord } from "@zhushanwen/pi-ext-guards";
|
|
14
|
+
|
|
13
15
|
/** 指纹 hash 长度(hex 字符数)。 */
|
|
14
|
-
|
|
16
|
+
const HASH_LEN = 16;
|
|
15
17
|
|
|
16
18
|
/** entry schema 版本(v2:短 hash + 增量 entry)。 */
|
|
17
19
|
export const SCHEMA_VERSION = 2;
|
|
@@ -41,12 +43,8 @@ export interface ProbeEntryData {
|
|
|
41
43
|
error?: string;
|
|
42
44
|
}
|
|
43
45
|
|
|
44
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
45
|
-
return value !== null && typeof value === "object";
|
|
46
|
-
}
|
|
47
|
-
|
|
48
46
|
/** 递归 sort keys 的稳定序列化(undefined 归一为 null,防 key 顺序抖动产生假变化)。 */
|
|
49
|
-
|
|
47
|
+
function stableStringify(value: unknown): string {
|
|
50
48
|
if (value === undefined) return "null";
|
|
51
49
|
if (value === null || typeof value !== "object") return JSON.stringify(value);
|
|
52
50
|
if (Array.isArray(value)) return "[" + value.map(stableStringify).join(",") + "]";
|
|
@@ -63,7 +61,12 @@ export function hashOf(value: unknown): string {
|
|
|
63
61
|
* - OpenAI 兼容:messages 内 role 为 system 或 developer
|
|
64
62
|
* (pi 源码 ai/src/api/openai-completions.ts 的 useDeveloperRole 分支)
|
|
65
63
|
* - Anthropic:顶层 system(数组)
|
|
66
|
-
* - Google
|
|
64
|
+
* - Google:顶层字段 systemInstruction 为对象形态——仅手工伪造的输入可达:
|
|
65
|
+
* pi-ai 0.84.4 实装(google-generative-ai.js:294 / google-vertex.js:368)恒传
|
|
66
|
+
* string(sanitizeSurrogates(context.systemPrompt)),isRecord 分支对 string
|
|
67
|
+
* 恒不命中,故实装流量下 Google 侧返回 null(spFull 观测恒 no-system)。
|
|
68
|
+
* 归因是「提取只认对象形态」而非「Google 请求没有 system」,勿把下方
|
|
69
|
+
* isRecord(systemInstruction) 分支误读为 Google 口径主通路。
|
|
67
70
|
*/
|
|
68
71
|
export function extractSystem(payload: unknown): unknown | null {
|
|
69
72
|
if (!isRecord(payload)) return null;
|
|
@@ -89,7 +92,7 @@ export function sentToolsOf(payload: unknown): unknown[] | null {
|
|
|
89
92
|
return Array.isArray(tools) ? tools : null;
|
|
90
93
|
}
|
|
91
94
|
|
|
92
|
-
|
|
95
|
+
function diffFingerprints(cur: Fingerprints, last: Fingerprints | null): FingerprintKey[] {
|
|
93
96
|
if (last === null) return [];
|
|
94
97
|
return (Object.keys(cur) as FingerprintKey[]).filter((k) => last[k] !== cur[k]);
|
|
95
98
|
}
|
package/src/index.ts
CHANGED
|
@@ -9,18 +9,21 @@
|
|
|
9
9
|
* - before_agent_start(每 turn 1 次):算 7 个输入侧 hash 暂存 pending
|
|
10
10
|
* (systemPromptOptions 各字段 + getAllTools 注册表)
|
|
11
11
|
* - before_provider_request(每笔 LLM 请求,仅消费 turn 首笔):从最终 payload
|
|
12
|
-
* 提取 spFull(system 消息,兼容 system/developer/anthropic/google
|
|
12
|
+
* 提取 spFull(system 消息,兼容 system/developer/anthropic/google 口径;
|
|
13
|
+
* google 口径实装流量恒 no-system——pi-ai 恒传 string,该分支仅手工伪造
|
|
14
|
+
* 对象输入可达,详见 fingerprint.ts extractSystem 注释)与
|
|
13
15
|
* toolsSent(tools 数组),与 pending 合并对比,变化/基线时 appendEntry
|
|
14
16
|
* - agent_end:turn 内无 provider 请求则丢弃 pending(无请求即无归因价值)
|
|
15
17
|
*
|
|
16
|
-
* seq = 进程内 before_agent_start
|
|
17
|
-
*
|
|
18
|
+
* seq = 进程内 before_agent_start 触发计数,随 entry 落盘供人工排序诊断;
|
|
19
|
+
* 当前无自动消费方(analyze.py 不读 seq)。
|
|
18
20
|
*
|
|
19
21
|
* Fail-safe:handler 全程捕获,异常时写 error entry(缺口可见非静默)并重置为
|
|
20
22
|
* 需基线状态;appendEntry 自身失败仅 stderr 诊断,绝不阻塞请求。
|
|
21
23
|
*/
|
|
22
24
|
|
|
23
25
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
26
|
+
import { toErrorMessage } from "@zhushanwen/pi-ext-guards";
|
|
24
27
|
|
|
25
28
|
import {
|
|
26
29
|
buildProbeEntry,
|
|
@@ -60,12 +63,12 @@ export default function (pi: ExtensionAPI): void {
|
|
|
60
63
|
// 输入侧 7 hash 计算在 fingerprint.ts 纯函数层(handler 只做编排与 fail-safe)
|
|
61
64
|
pending = computePendingFingerprint(event?.systemPromptOptions, pi.getAllTools());
|
|
62
65
|
} catch (err) {
|
|
63
|
-
stderr(`before_agent_start failed: ${
|
|
66
|
+
stderr(`before_agent_start failed: ${toErrorMessage(err)}`);
|
|
64
67
|
pending = null;
|
|
65
68
|
try {
|
|
66
|
-
pi.appendEntry(CUSTOM_TYPE, { v: SCHEMA_VERSION, seq, error:
|
|
69
|
+
pi.appendEntry(CUSTOM_TYPE, { v: SCHEMA_VERSION, seq, error: toErrorMessage(err) });
|
|
67
70
|
} catch (appendErr) {
|
|
68
|
-
stderr(`appendEntry failed: ${
|
|
71
|
+
stderr(`appendEntry failed: ${toErrorMessage(appendErr)}`);
|
|
69
72
|
}
|
|
70
73
|
needsBaseline = true;
|
|
71
74
|
}
|
|
@@ -88,12 +91,12 @@ export default function (pi: ExtensionAPI): void {
|
|
|
88
91
|
last = cur;
|
|
89
92
|
needsBaseline = false;
|
|
90
93
|
} catch (err) {
|
|
91
|
-
const msg =
|
|
94
|
+
const msg = toErrorMessage(err);
|
|
92
95
|
stderr(`before_provider_request failed: ${msg}`);
|
|
93
96
|
try {
|
|
94
97
|
pi.appendEntry(CUSTOM_TYPE, { v: SCHEMA_VERSION, seq, error: msg });
|
|
95
98
|
} catch (appendErr) {
|
|
96
|
-
stderr(`appendEntry failed: ${
|
|
99
|
+
stderr(`appendEntry failed: ${toErrorMessage(appendErr)}`);
|
|
97
100
|
}
|
|
98
101
|
needsBaseline = true;
|
|
99
102
|
}
|