pi-multimodal-proxy 1.6.0 → 1.10.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/CHANGELOG.md +58 -0
- package/README.md +273 -220
- package/extensions/__tests__/compaction.test.ts +218 -0
- package/extensions/__tests__/integration.test.ts +1 -3
- package/extensions/__tests__/internal.test.ts +814 -16
- package/extensions/__tests__/recall-autocomplete.test.ts +137 -0
- package/extensions/internal.ts +1051 -52
- package/extensions/vision-proxy.ts +3056 -2326
- package/package.json +3 -3
- package/.pi/ghost-autocomplete/metrics.jsonl +0 -192
- package/.pi/ghost-autocomplete/profile.jsonl +0 -19
- package/PRD-Implementation-Status.md +0 -170
- package/PRD.md +0 -599
- package/bash.exe.stackdump +0 -28
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { describe, it } from "node:test";
|
|
3
|
+
import {
|
|
4
|
+
buildCompactionDigest,
|
|
5
|
+
collectVisibleFenceIds,
|
|
6
|
+
CUSTOM_TYPE_DESCRIPTION,
|
|
7
|
+
CUSTOM_TYPE_VIDEO_DESCRIPTION,
|
|
8
|
+
DIGEST_IMAGE_CHARS,
|
|
9
|
+
DIGEST_LEAN_IMAGE_CHARS,
|
|
10
|
+
DIGEST_MAX_IMAGES,
|
|
11
|
+
findDescriptions,
|
|
12
|
+
findVideoDescriptions,
|
|
13
|
+
truncateForDigest,
|
|
14
|
+
type VideoDescriptionEntry,
|
|
15
|
+
} from "../internal.ts";
|
|
16
|
+
|
|
17
|
+
// SessionEntry minimal shape — typed loose because peer dep types are not loaded in test
|
|
18
|
+
type Entry = any;
|
|
19
|
+
|
|
20
|
+
const customEntry = (customType: string, data: unknown): Entry => ({
|
|
21
|
+
type: "custom",
|
|
22
|
+
customType,
|
|
23
|
+
data,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const video = (hash: string, description = `video ${hash}`): VideoDescriptionEntry => ({
|
|
27
|
+
hash,
|
|
28
|
+
filename: `${hash}.mp4`,
|
|
29
|
+
mimeType: "video/mp4",
|
|
30
|
+
description,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
describe("findVideoDescriptions", () => {
|
|
34
|
+
it("collects hash → entry from video description entries", () => {
|
|
35
|
+
const entries: Entry[] = [
|
|
36
|
+
customEntry(CUSTOM_TYPE_VIDEO_DESCRIPTION, video("v1")),
|
|
37
|
+
customEntry(CUSTOM_TYPE_DESCRIPTION, { hash: "img", description: "not a video" }),
|
|
38
|
+
customEntry(CUSTOM_TYPE_VIDEO_DESCRIPTION, { hash: "", description: "skip" }),
|
|
39
|
+
customEntry(CUSTOM_TYPE_VIDEO_DESCRIPTION, video("v2")),
|
|
40
|
+
];
|
|
41
|
+
const map = findVideoDescriptions(entries);
|
|
42
|
+
assert.equal(map.size, 2);
|
|
43
|
+
assert.equal(map.get("v1")?.filename, "v1.mp4");
|
|
44
|
+
assert.equal(map.get("v2")?.mimeType, "video/mp4");
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("keeps the most recent entry per hash", () => {
|
|
48
|
+
const entries: Entry[] = [
|
|
49
|
+
customEntry(CUSTOM_TYPE_VIDEO_DESCRIPTION, video("v1", "old")),
|
|
50
|
+
customEntry(CUSTOM_TYPE_VIDEO_DESCRIPTION, video("v1", "new")),
|
|
51
|
+
];
|
|
52
|
+
assert.equal(findVideoDescriptions(entries).get("v1")?.description, "new");
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("moves a re-described hash to the end of the iteration order", () => {
|
|
56
|
+
const entries: Entry[] = [
|
|
57
|
+
customEntry(CUSTOM_TYPE_VIDEO_DESCRIPTION, video("v1", "first")),
|
|
58
|
+
customEntry(CUSTOM_TYPE_VIDEO_DESCRIPTION, video("v2", "second")),
|
|
59
|
+
customEntry(CUSTOM_TYPE_VIDEO_DESCRIPTION, video("v1", "updated")),
|
|
60
|
+
];
|
|
61
|
+
assert.deepEqual([...findVideoDescriptions(entries).keys()], ["v2", "v1"]);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("backfills filename and mimeType on malformed entries instead of crashing later", () => {
|
|
65
|
+
const entries: Entry[] = [
|
|
66
|
+
customEntry(CUSTOM_TYPE_VIDEO_DESCRIPTION, { hash: "v1", description: "d" }),
|
|
67
|
+
customEntry(CUSTOM_TYPE_VIDEO_DESCRIPTION, { hash: "v2", description: "d", filename: 42, mimeType: null }),
|
|
68
|
+
];
|
|
69
|
+
const map = findVideoDescriptions(entries);
|
|
70
|
+
assert.equal(map.get("v1")?.filename, "unknown");
|
|
71
|
+
assert.equal(map.get("v1")?.mimeType, "application/octet-stream");
|
|
72
|
+
assert.equal(map.get("v2")?.filename, "unknown");
|
|
73
|
+
assert.equal(map.get("v2")?.mimeType, "application/octet-stream");
|
|
74
|
+
// The digest builder accepts the backfilled entries without throwing.
|
|
75
|
+
const digest = buildCompactionDigest([], [...map.values()]);
|
|
76
|
+
assert.ok(digest.includes('file="unknown"'));
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe("findDescriptions ordering", () => {
|
|
81
|
+
it("moves a re-described hash to the end of the iteration order", () => {
|
|
82
|
+
const entries: Entry[] = [
|
|
83
|
+
customEntry(CUSTOM_TYPE_DESCRIPTION, { hash: "a", description: "first" }),
|
|
84
|
+
customEntry(CUSTOM_TYPE_DESCRIPTION, { hash: "b", description: "second" }),
|
|
85
|
+
customEntry(CUSTOM_TYPE_DESCRIPTION, { hash: "a", description: "updated" }),
|
|
86
|
+
];
|
|
87
|
+
const map = findDescriptions(entries);
|
|
88
|
+
assert.deepEqual([...map.keys()], ["b", "a"]);
|
|
89
|
+
assert.equal(map.get("a"), "updated");
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
describe("truncateForDigest", () => {
|
|
94
|
+
it("returns short text unchanged", () => {
|
|
95
|
+
assert.equal(truncateForDigest("short text", 100), "short text");
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("trims surrounding whitespace", () => {
|
|
99
|
+
assert.equal(truncateForDigest(" padded ", 100), "padded");
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("cuts long text at a word boundary and marks the truncation", () => {
|
|
103
|
+
// slice(0,20) is "alpha beta gamma del"; the boundary cut must drop "del".
|
|
104
|
+
const out = truncateForDigest("alpha beta gamma delta epsilon", 20);
|
|
105
|
+
assert.equal(out, "alpha beta gamma … [truncated]");
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("hard-cuts when there is no usable word boundary", () => {
|
|
109
|
+
const out = truncateForDigest("a".repeat(50), 20);
|
|
110
|
+
assert.equal(out, `${"a".repeat(20)} … [truncated]`);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("never splits a surrogate pair on a hard cut", () => {
|
|
114
|
+
// "💥" is 2 UTF-16 code units; a cut at max=21 would land mid-pair.
|
|
115
|
+
const out = truncateForDigest(`${"x".repeat(20)}💥${"y".repeat(30)}`, 21);
|
|
116
|
+
assert.ok(!/[\uD800-\uDBFF]$/.test(out.replace(" … [truncated]", "")));
|
|
117
|
+
assert.ok(!out.includes("�"));
|
|
118
|
+
assert.equal(out, `${"x".repeat(20)} … [truncated]`);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
describe("collectVisibleFenceIds", () => {
|
|
123
|
+
const h1 = "a".repeat(32);
|
|
124
|
+
const h2 = "b".repeat(32);
|
|
125
|
+
const h3 = "c".repeat(32);
|
|
126
|
+
|
|
127
|
+
it("collects ids from description, analysis, joint, and video fences", () => {
|
|
128
|
+
const text = [
|
|
129
|
+
`<vision_proxy_description image="${h1}" width="10"\n>body\n</vision_proxy_description>`,
|
|
130
|
+
`<vision_proxy_analysis image="${h2}#crop:1,2,3,4"\n>body\n</vision_proxy_analysis>`,
|
|
131
|
+
`dimensions='[{"image":"${h3}","width":5}]'`,
|
|
132
|
+
`<vision_proxy_video_description file="v.mp4" hash="${h1}" mime="video/mp4"\n>t\n</vision_proxy_video_description>`,
|
|
133
|
+
].join("\n");
|
|
134
|
+
const ids = collectVisibleFenceIds(text);
|
|
135
|
+
assert.deepEqual([...ids].sort(), [h1, h2, h3]);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("ignores bare hashes and user-typed recall references", () => {
|
|
139
|
+
const ids = collectVisibleFenceIds(`zoom into image="${h1}" and also ${h2} please`);
|
|
140
|
+
assert.equal(ids.size, 0);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("accumulates into a provided set", () => {
|
|
144
|
+
const out = new Set<string>();
|
|
145
|
+
collectVisibleFenceIds(`<vision_proxy_description image="${h1}"\n>x\n</vision_proxy_description>`, out);
|
|
146
|
+
collectVisibleFenceIds(`<vision_proxy_description image="${h2}"\n>x\n</vision_proxy_description>`, out);
|
|
147
|
+
assert.equal(out.size, 2);
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
describe("buildCompactionDigest", () => {
|
|
152
|
+
it("returns empty string when there is nothing to restore", () => {
|
|
153
|
+
assert.equal(buildCompactionDigest([], []), "");
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it("restores image fences with id, meta, and untrusted warning", () => {
|
|
157
|
+
const digest = buildCompactionDigest(
|
|
158
|
+
[{ hash: "hash-a", description: "a red square", meta: { width: 10, height: 20, filename: "sq.png" } }],
|
|
159
|
+
[],
|
|
160
|
+
);
|
|
161
|
+
assert.ok(digest.includes("post-compaction recall"));
|
|
162
|
+
assert.ok(digest.includes('image="hash-a"'));
|
|
163
|
+
assert.ok(digest.includes('width="10"'));
|
|
164
|
+
assert.ok(digest.includes('filename="sq.png"'));
|
|
165
|
+
assert.ok(digest.includes("a red square"));
|
|
166
|
+
assert.ok(digest.includes("UNTRUSTED"));
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it("includes video fences", () => {
|
|
170
|
+
const digest = buildCompactionDigest([], [video("v9", "a talking head")]);
|
|
171
|
+
assert.ok(digest.includes('hash="v9"'));
|
|
172
|
+
assert.ok(digest.includes('file="v9.mp4"'));
|
|
173
|
+
assert.ok(digest.includes("a talking head"));
|
|
174
|
+
assert.ok(digest.includes("1 video/audio file"));
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it("mentions analyze_image recall only when the tool is enabled", () => {
|
|
178
|
+
const imgs = [{ hash: "h", description: "d" }];
|
|
179
|
+
assert.ok(buildCompactionDigest(imgs, [], { toolEnabled: true }).includes("analyze_image"));
|
|
180
|
+
assert.ok(!buildCompactionDigest(imgs, [], { toolEnabled: false }).includes("analyze_image"));
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("keeps only the most recent images when over the cap", () => {
|
|
184
|
+
const imgs = Array.from({ length: DIGEST_MAX_IMAGES + 3 }, (_, i) => ({
|
|
185
|
+
hash: `h${i}`,
|
|
186
|
+
description: `d${i}`,
|
|
187
|
+
}));
|
|
188
|
+
const digest = buildCompactionDigest(imgs, []);
|
|
189
|
+
assert.ok(!digest.includes('image="h0"'), "oldest images dropped");
|
|
190
|
+
assert.ok(!digest.includes('image="h2"'));
|
|
191
|
+
assert.ok(digest.includes('image="h3"'));
|
|
192
|
+
assert.ok(digest.includes(`image="h${DIGEST_MAX_IMAGES + 2}"`));
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it("applies tighter budgets in lean mode", () => {
|
|
196
|
+
const long = "word ".repeat(400);
|
|
197
|
+
const [full] = buildCompactionDigest([{ hash: "h", description: long }], [])
|
|
198
|
+
.split("\n\n")
|
|
199
|
+
.slice(1);
|
|
200
|
+
const [lean] = buildCompactionDigest([{ hash: "h", description: long }], [], { lean: true })
|
|
201
|
+
.split("\n\n")
|
|
202
|
+
.slice(1);
|
|
203
|
+
assert.ok(full!.length > lean!.length);
|
|
204
|
+
assert.ok(lean!.length < DIGEST_LEAN_IMAGE_CHARS + 200);
|
|
205
|
+
assert.ok(full!.length < DIGEST_IMAGE_CHARS + 200);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it("counts both media kinds in the header", () => {
|
|
209
|
+
const digest = buildCompactionDigest(
|
|
210
|
+
[
|
|
211
|
+
{ hash: "a", description: "d1" },
|
|
212
|
+
{ hash: "b", description: "d2" },
|
|
213
|
+
],
|
|
214
|
+
[video("v1"), video("v2")],
|
|
215
|
+
);
|
|
216
|
+
assert.ok(digest.includes("2 images and 2 video/audio files"));
|
|
217
|
+
});
|
|
218
|
+
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Integration tests for pi-
|
|
2
|
+
* Integration tests for pi-multimodal-proxy.
|
|
3
3
|
*
|
|
4
4
|
* These tests mock Pi's extension interfaces to test the full wiring:
|
|
5
5
|
* fence output format, consent flow, tool validation, context stripping,
|
|
@@ -22,8 +22,6 @@ import {
|
|
|
22
22
|
bufferToPiAiImage,
|
|
23
23
|
resolveCropEntry,
|
|
24
24
|
hashImageData,
|
|
25
|
-
_imageMeta,
|
|
26
|
-
storeImageMeta,
|
|
27
25
|
extractDimensions,
|
|
28
26
|
computePHash,
|
|
29
27
|
hammingDistance,
|