pi-vision-handoff 0.7.0 → 0.7.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/package.json +1 -1
- package/vision-handoff.ts +10 -68
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-vision-handoff",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.1",
|
|
4
4
|
"description": "Give text-only pi models vision — describe images with a vision model you pick via an interactive picker, then hand off the text description to non-vision models",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"author": "Tom X Nguyen",
|
package/vision-handoff.ts
CHANGED
|
@@ -56,18 +56,6 @@ import { resizeImage } from "@earendil-works/pi-coding-agent";
|
|
|
56
56
|
import { VisionModelSelectorComponent, type VisionModelSelectorResult } from "./src/vision-model-selector.js";
|
|
57
57
|
import { PrewarmEditor } from "./src/prewarm-editor.js";
|
|
58
58
|
|
|
59
|
-
/**
|
|
60
|
-
* pi-fabric prepends this prefix to every nested tool-call id it generates
|
|
61
|
-
* inside a fabric_exec run (one per pi.* invocation in full-code mode). The
|
|
62
|
-
* LLM's own tool-call ids (e.g. openai "call_…", anthropic "toolu_…") never use
|
|
63
|
-
* it, so a tool_result whose toolCallId starts with this prefix is a nested
|
|
64
|
-
* fabric call whose result returns to the sandbox program — not the agent's
|
|
65
|
-
* message context — so the context hook will never see its image blocks. We
|
|
66
|
-
* therefore swap image→description inline in the tool_result handler for
|
|
67
|
-
* nested calls. Mirror of pi-fabric's NESTED_TOOL_CALL_ID_PREFIX.
|
|
68
|
-
*/
|
|
69
|
-
const FABRIC_NESTED_TOOL_CALL_ID_PREFIX = "fabric_";
|
|
70
|
-
|
|
71
59
|
let config: VisionHandoffConfig = readConfig();
|
|
72
60
|
|
|
73
61
|
/** Most recent describer failure message (auth error, network error, abort,
|
|
@@ -363,21 +351,15 @@ export default function (pi: ExtensionAPI) {
|
|
|
363
351
|
// iteration, so reads completing together land in ONE batch and all resolve
|
|
364
352
|
// together.
|
|
365
353
|
//
|
|
366
|
-
//
|
|
367
|
-
//
|
|
368
|
-
//
|
|
369
|
-
//
|
|
370
|
-
//
|
|
371
|
-
//
|
|
372
|
-
//
|
|
373
|
-
//
|
|
374
|
-
//
|
|
375
|
-
// nested result is transient (never rendered by kitty or stored in
|
|
376
|
-
// /resume). So we SWAP image→description inline here, mirroring the
|
|
377
|
-
// `context` handler, so the description becomes pi.read's return value:
|
|
378
|
-
// pi-fabric's normalizeResult extracts the text and the text-only agent
|
|
379
|
-
// receives the description instead of a dropped image block. Nested
|
|
380
|
-
// fabric calls are tagged with a `fabric_`-prefixed toolCallId.
|
|
354
|
+
// Both a direct agent `read` and a NESTED `pi.read` inside fabric_exec reach
|
|
355
|
+
// this handler and take the SAME path: WARM the cache (during the free
|
|
356
|
+
// tool-result phase), strip pi's misleading non-vision note, and KEEP the
|
|
357
|
+
// image block so kitty renders it inline and /resume retains it. The
|
|
358
|
+
// `context` hook swaps image→description on the LLM-bound clone before the
|
|
359
|
+
// next turn, so the text-only agent receives the description as text. This
|
|
360
|
+
// holds for fabric too: pi-fabric re-attaches the nested read's image to the
|
|
361
|
+
// fabric_exec tool-result content, which IS the agent's message context — so
|
|
362
|
+
// the `context` hook sees and swaps it, exactly like a direct read.
|
|
381
363
|
pi.on("tool_result", async (event, ctx) => {
|
|
382
364
|
if (!isConfigured(config)) return;
|
|
383
365
|
if (event.toolName !== "read") return;
|
|
@@ -416,47 +398,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
416
398
|
|
|
417
399
|
warnFailedImages(ctx, imgs, descs, lastDescriberError ?? "unknown error");
|
|
418
400
|
|
|
419
|
-
//
|
|
420
|
-
// comment). The descriptions are already wrapped ([Image: …]) by the
|
|
421
|
-
// dataloader, so push them directly; strip pi's non-vision note from any
|
|
422
|
-
// surviving text blocks since the description replaces the omitted image.
|
|
423
|
-
if (
|
|
424
|
-
typeof event.toolCallId === "string" &&
|
|
425
|
-
event.toolCallId.startsWith(FABRIC_NESTED_TOOL_CALL_ID_PREFIX)
|
|
426
|
-
) {
|
|
427
|
-
const descByHash = new Map<string, string>();
|
|
428
|
-
for (let i = 0; i < imgs.length; i++) {
|
|
429
|
-
descByHash.set(imageHash(imgs[i].mimeType, imgs[i].data), descs[i]);
|
|
430
|
-
}
|
|
431
|
-
const next: (TextContent | ImageContent)[] = [];
|
|
432
|
-
for (const block of content) {
|
|
433
|
-
const img = extractImageFromBlock(block);
|
|
434
|
-
if (img) {
|
|
435
|
-
next.push({
|
|
436
|
-
type: "text",
|
|
437
|
-
text: descByHash.get(imageHash(img.mimeType, img.data)) ?? UNAVAILABLE,
|
|
438
|
-
});
|
|
439
|
-
continue;
|
|
440
|
-
}
|
|
441
|
-
if (
|
|
442
|
-
block &&
|
|
443
|
-
typeof block === "object" &&
|
|
444
|
-
(block as { type: string }).type === "text" &&
|
|
445
|
-
typeof (block as { text: string }).text === "string" &&
|
|
446
|
-
(block as { text: string }).text.includes(NON_VISION_IMAGE_NOTE)
|
|
447
|
-
) {
|
|
448
|
-
next.push({
|
|
449
|
-
type: "text",
|
|
450
|
-
text: stripNonVisionImageNote((block as { text: string }).text),
|
|
451
|
-
});
|
|
452
|
-
} else {
|
|
453
|
-
next.push(block as TextContent | ImageContent);
|
|
454
|
-
}
|
|
455
|
-
}
|
|
456
|
-
return { content: next };
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
// Normal flow: warm the cache (done above) and strip pi's
|
|
401
|
+
// Warm the cache (done above) and strip pi's
|
|
460
402
|
// `[Current model does not support images…]` note from text blocks —
|
|
461
403
|
// since the handoff replaces the image with a description in the
|
|
462
404
|
// `context` hook, that note is misleading (the agent WILL receive the
|