pi-vision-handoff 0.9.1 → 0.9.3

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 CHANGED
@@ -255,7 +255,7 @@ Because pi runs parallel `read` tool calls via `Promise.all` and fires each read
255
255
  | `context` (all messages) | `{ type: "input_image", image_url: "data:…" }` (OpenAI Responses) | detected by shape → replaced with `{ type: "input_text", text }` |
256
256
  | `context` (all messages) | `{ type: "image", source: { type: "base64", media_type, data } }` (Anthropic Messages) | detected by shape → replaced with `{ type: "text", text }` |
257
257
 
258
- The describer call itself goes through pi's normal model machinery (`completeSimple()`), **not** the agent event loop — so it never re-triggers `context` (no recursion). The `read` tool result keeps its image block untouched (kitty inline + `/resume`); only the `context`-cloned LLM-bound payload has images swapped for text.
258
+ The describer call itself goes through pi's normal model machinery (`completeSimple()`), **not** the agent event loop — so it never re-triggers `context` (no recursion). Every real describer request receives a dedicated helper session ID. For Neuralwatt, the request also receives a matching isolated `X-NW-Conversation-ID` instead of inheriting the main Pi session ID; the unrelated image prompt therefore cannot replace the main agent's server-side cache lineage. The `read` tool result keeps its image block untouched (kitty inline + `/resume`); only the `context`-cloned LLM-bound payload has images swapped for text.
259
259
 
260
260
  ### Usage reporting
261
261
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-vision-handoff",
3
- "version": "0.9.1",
3
+ "version": "0.9.3",
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",
@@ -31,9 +31,9 @@
31
31
  "README.md"
32
32
  ],
33
33
  "devDependencies": {
34
- "@earendil-works/pi-ai": "0.80.10",
35
- "@earendil-works/pi-coding-agent": "0.80.10",
36
- "@earendil-works/pi-tui": "0.80.10",
34
+ "@earendil-works/pi-ai": "0.83.0",
35
+ "@earendil-works/pi-coding-agent": "0.83.0",
36
+ "@earendil-works/pi-tui": "0.83.0",
37
37
  "@types/node": "25.9.1",
38
38
  "@vitest/coverage-v8": "4.1.7",
39
39
  "knip": "6.14.1",
package/src/describer.ts CHANGED
@@ -60,6 +60,7 @@ import {
60
60
  type VisionHandoffUsageRecord,
61
61
  } from "./usage.js";
62
62
  import { imageHash } from "./image.js";
63
+ import { randomUUID } from "node:crypto";
63
64
  import { abortWireGuard, fetchInterceptorGuard, timeoutGuard, type AbortWire } from "./dispose.js";
64
65
 
65
66
  /** Tokens reserved for the describer's input so the requested output budget
@@ -133,17 +134,47 @@ function configSnapshot(cfg: VisionHandoffConfig): {
133
134
  * completeSimple() directly cannot resolve custom API ids such as `makora`.
134
135
  * The optional access preserves compatibility with older ModelRegistry versions,
135
136
  * which registered custom streams globally and do not expose the config getter. */
137
+ const NEURALWATT_CONVERSATION_HEADER = "x-nw-conversation-id";
138
+
139
+ function isolateVisionRequest(
140
+ model: Model<Api>,
141
+ options: SimpleStreamOptions,
142
+ ): SimpleStreamOptions {
143
+ const sessionId = `pi-vision-handoff:${randomUUID()}`;
144
+ const headers = { ...(options.headers ?? {}) };
145
+ const inheritedConversationHeader = Object.keys(headers).find(
146
+ (name) => name.toLowerCase() === NEURALWATT_CONVERSATION_HEADER,
147
+ );
148
+
149
+ // Provider auth can carry the active Pi conversation id. A describer call is
150
+ // an independent conversation: reusing that id makes an unrelated image
151
+ // prompt the newest cache lineage for the main agent. Give every real vision
152
+ // request its own identity while preserving all non-session auth headers.
153
+ if (inheritedConversationHeader) {
154
+ headers[inheritedConversationHeader] = sessionId;
155
+ } else if (model.provider.toLowerCase() === "neuralwatt") {
156
+ headers["X-NW-Conversation-ID"] = sessionId;
157
+ }
158
+
159
+ return {
160
+ ...options,
161
+ sessionId,
162
+ ...(Object.keys(headers).length > 0 ? { headers } : {}),
163
+ };
164
+ }
165
+
136
166
  export async function completeVisionModel(
137
167
  model: Model<Api>,
138
168
  modelRegistry: ModelRegistry,
139
169
  context: Context,
140
170
  options: SimpleStreamOptions,
141
171
  ): Promise<AssistantMessage> {
172
+ const isolatedOptions = isolateVisionRequest(model, options);
142
173
  const provider = modelRegistry.getRegisteredProviderConfig?.(model.provider);
143
174
  if (provider?.streamSimple && provider.api === model.api) {
144
- return provider.streamSimple(model, context, options).result();
175
+ return provider.streamSimple(model, context, isolatedOptions).result();
145
176
  }
146
- return completeSimple(model, context, options);
177
+ return completeSimple(model, context, isolatedOptions);
147
178
  }
148
179
 
149
180
  /** Dependencies the describer can't own itself (held by the engine). */