pi-vision-handoff 0.6.1 → 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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/vision-handoff.ts +18 -25
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-vision-handoff",
3
- "version": "0.6.1",
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
@@ -349,15 +349,17 @@ export default function (pi: ExtensionAPI) {
349
349
  // `tool_result` as its I/O completes (poll phase), and the loader's
350
350
  // `setImmediate` dispatch defers to the check phase AFTER the whole poll
351
351
  // iteration, so reads completing together land in ONE batch and all resolve
352
- // together. The descriptions replace the image blocks in the returned
353
- // `content`, so by the time the agent's next turn starts the tool results
354
- // already carry text — the agent never sees raw image blocks it can't
355
- // process.
352
+ // together.
356
353
  //
357
- // Why block here and not in `context`: the tool-result phase is free time
358
- // (the agent is just waiting for tool results), so running the describer
359
- // here adds zero latency to the critical path. `context` then becomes a
360
- // cache-hit no-op for read images.
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.
361
363
  pi.on("tool_result", async (event, ctx) => {
362
364
  if (!isConfigured(config)) return;
363
365
  if (event.toolName !== "read") return;
@@ -385,19 +387,9 @@ export default function (pi: ExtensionAPI) {
385
387
  // loader's `setImmediate` dispatch defers to the check phase, AFTER the
386
388
  // whole poll iteration, so reads completing together (the common case for
387
389
  // cached local files) land in ONE batch — ONE vision call for the whole
388
- // read set, not N. Reads completing in separate iterations get separate
389
- // calls, but always in parallel, never sequential. Awaiting here runs the
390
- // describer during the tool-result phase (free time — the agent is just
391
- // waiting for tool results), so the batch is COMPLETE before `context`
392
- // fires, making `context` a non-blocking cache hit instead of a cold miss
393
- // on the critical path.
394
- //
395
- // We do NOT mutate the result content here for the image blocks: returning
396
- // undefined keeps the image block in storage so kitty renders it inline
397
- // and `/resume` retains it. The actual image→text swap happens in the
398
- // `context` hook (on the cloned LLM-bound payload only), by which point
399
- // these are cache hits. We DO strip pi's misleading non-vision note (below)
400
- // since the handoff will replace the image with a description.
390
+ // read set, not N. Awaiting here runs the describer during the tool-result
391
+ // phase (free time), so the batch is COMPLETE before `context` fires,
392
+ // making `context` a non-blocking cache hit instead of a cold miss.
401
393
  const descs = await Promise.all(imgs.map((img) => loader.loadDescription(img)));
402
394
 
403
395
  // On user abort, leave the result untouched — pi is tearing the turn
@@ -406,12 +398,13 @@ export default function (pi: ExtensionAPI) {
406
398
 
407
399
  warnFailedImages(ctx, imgs, descs, lastDescriberError ?? "unknown error");
408
400
 
409
- // Strip pi's `[Current model does not support images…]` note from text
410
- // blocks — since the handoff replaces the image with a description in the
401
+ // Warm the cache (done above) and strip pi's
402
+ // `[Current model does not support images…]` note from text blocks —
403
+ // since the handoff replaces the image with a description in the
411
404
  // `context` hook, that note is misleading (the agent WILL receive the
412
405
  // image's content, as text). Keep the image block itself so kitty still
413
- // renders it inline and `/resume` retains it; the `context` hook swaps the
414
- // image for its description in the LLM-bound clone before the next turn.
406
+ // renders it inline and `/resume` retains it; the `context` hook swaps
407
+ // the image for its description in the LLM-bound clone before the next turn.
415
408
  let stripped = false;
416
409
  const next = content.slice();
417
410
  for (let i = 0; i < next.length; i++) {