pi-vision-handoff 0.5.0 → 0.5.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/src/dataloader.ts +59 -8
- package/vision-handoff.ts +19 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-vision-handoff",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.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/src/dataloader.ts
CHANGED
|
@@ -91,19 +91,68 @@ export class DescriptionLoader implements Disposable {
|
|
|
91
91
|
private dispatchScheduled = false;
|
|
92
92
|
private turnModelRegistry: ModelRegistry | null = null;
|
|
93
93
|
private turnVisionModel: Model<Api> | null = null;
|
|
94
|
-
|
|
94
|
+
/** Turn-level abort controller. In-flight describer batches (`runBatch`)
|
|
95
|
+
* are wired to `turnAbortController.signal` so a user cancel (ESC) aborts
|
|
96
|
+
* them EVEN WHEN the batch was dispatched before the run's live abort
|
|
97
|
+
* signal existed — the `before_agent_start` / paste-time prewarm case,
|
|
98
|
+
* where `ctx.signal` is `undefined` (the agent run hasn't started yet).
|
|
99
|
+
* The run's live signal, once it arrives via `bindTurnContext`, is forwarded
|
|
100
|
+
* into `turnAbortController.abort()`. Because `runBatch` holds a reference
|
|
101
|
+
* to this controller's signal (not a snapshot of `ctx.signal`), a batch
|
|
102
|
+
* that started during prewarm becomes abortable the instant a later
|
|
103
|
+
* `bindTurnContext` (from `tool_result`/`context`) brings the live signal.
|
|
104
|
+
* Reset per turn via {@link resetTurnAbort} so a previous turn's cancel
|
|
105
|
+
* can't poison the next turn's prewarm. */
|
|
106
|
+
private turnAbortController = new AbortController();
|
|
107
|
+
/** The run signal currently forwarded into {@link turnAbortController}, so
|
|
108
|
+
* the abort listener is attached at most once per signal (the same signal
|
|
109
|
+
* is bound by every `tool_result`/`context` event in a turn). */
|
|
110
|
+
private wiredSignal: AbortSignal | undefined;
|
|
95
111
|
private pendingTurnPrompt = "";
|
|
96
112
|
|
|
97
113
|
constructor(private readonly deps: LoaderDeps) {}
|
|
98
114
|
|
|
99
|
-
/** Bind the turn context (model registry, resolved vision model, abort
|
|
100
|
-
* for the loader's next dispatch. Called from every handler that
|
|
101
|
-
* `loadDescription()`.
|
|
115
|
+
/** Bind the turn context (model registry, resolved vision model, abort
|
|
116
|
+
* signal) for the loader's next dispatch. Called from every handler that
|
|
117
|
+
* may trigger `loadDescription()`.
|
|
118
|
+
*
|
|
119
|
+
* The abort signal is forwarded into the loader's {@link turnAbortController}
|
|
120
|
+
* rather than stored directly. Storing `ctx.signal` directly would leave a
|
|
121
|
+
* prewarm-dispatched batch (started in `before_agent_start`, where
|
|
122
|
+
* `ctx.signal` is `undefined` because the run hasn't started) with no abort
|
|
123
|
+
* wire — ESC couldn't cancel it, so the `tool_result` handler would only
|
|
124
|
+
* discard the result AFTER the vision call ran to completion. Forwarding
|
|
125
|
+
* the live signal into a stable, loader-owned controller lets an in-flight
|
|
126
|
+
* prewarm batch be aborted the moment the live signal arrives. */
|
|
102
127
|
bindTurnContext(ctx: { modelRegistry: ModelRegistry; signal?: AbortSignal }): void {
|
|
103
128
|
this.turnModelRegistry = ctx.modelRegistry;
|
|
104
|
-
this.turnSignal = ctx.signal;
|
|
105
129
|
const resolved = this.deps.resolveVisionModel(ctx.modelRegistry, this.deps.getConfig().visionModel!);
|
|
106
130
|
if (resolved) this.turnVisionModel = resolved;
|
|
131
|
+
const signal = ctx.signal;
|
|
132
|
+
if (!signal) return;
|
|
133
|
+
if (signal.aborted) {
|
|
134
|
+
this.turnAbortController.abort();
|
|
135
|
+
this.wiredSignal = signal;
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
if (signal === this.wiredSignal) return; // already forwarded this signal
|
|
139
|
+
this.wiredSignal = signal;
|
|
140
|
+
signal.addEventListener("abort", () => this.turnAbortController.abort(), { once: true });
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Reset the turn-level abort controller for a fresh turn. Call at turn
|
|
144
|
+
* boundaries (`before_agent_start`, paste-time prewarm) so a previous turn's
|
|
145
|
+
* cancel doesn't leave {@link turnAbortController} aborted — which would
|
|
146
|
+
* make every subsequent dispatch short-circuit to UNAVAILABLE. A
|
|
147
|
+
* non-aborted controller is reused (avoids orphaning an in-flight paste
|
|
148
|
+
* prewarm holding its signal); only an aborted one is replaced. `wiredSignal`
|
|
149
|
+
* is always cleared so the next live signal re-wires. Safe at a real turn
|
|
150
|
+
* boundary, where the prior turn's batch has settled. */
|
|
151
|
+
resetTurnAbort(): void {
|
|
152
|
+
if (this.turnAbortController.signal.aborted) {
|
|
153
|
+
this.turnAbortController = new AbortController();
|
|
154
|
+
}
|
|
155
|
+
this.wiredSignal = undefined;
|
|
107
156
|
}
|
|
108
157
|
|
|
109
158
|
/** Capture this turn's user prompt so every image in the turn is described in
|
|
@@ -160,7 +209,9 @@ export class DescriptionLoader implements Disposable {
|
|
|
160
209
|
this.dispatchScheduled = false;
|
|
161
210
|
this.turnModelRegistry = null;
|
|
162
211
|
this.turnVisionModel = null;
|
|
163
|
-
|
|
212
|
+
// Fresh controller on session reset (no in-flight batch to orphan).
|
|
213
|
+
this.turnAbortController = new AbortController();
|
|
214
|
+
this.wiredSignal = undefined;
|
|
164
215
|
this.pendingTurnPrompt = "";
|
|
165
216
|
}
|
|
166
217
|
|
|
@@ -194,7 +245,7 @@ export class DescriptionLoader implements Disposable {
|
|
|
194
245
|
for (const cb of batch.callbacks) cb.resolve(UNAVAILABLE);
|
|
195
246
|
return;
|
|
196
247
|
}
|
|
197
|
-
if (this.
|
|
248
|
+
if (this.turnAbortController.signal.aborted) {
|
|
198
249
|
for (const key of batch.keys) this.cache.delete(key);
|
|
199
250
|
for (const cb of batch.callbacks) cb.resolve(UNAVAILABLE);
|
|
200
251
|
return;
|
|
@@ -209,7 +260,7 @@ export class DescriptionLoader implements Disposable {
|
|
|
209
260
|
this.turnModelRegistry,
|
|
210
261
|
cfg,
|
|
211
262
|
this.deps,
|
|
212
|
-
this.
|
|
263
|
+
this.turnAbortController.signal,
|
|
213
264
|
);
|
|
214
265
|
// Build per-hash results, then fan them out to every callback. This reaches
|
|
215
266
|
// duplicate-hash callbacks that a key-indexed loop would have skipped (and
|
package/vision-handoff.ts
CHANGED
|
@@ -144,6 +144,13 @@ function prewarmClipboardPath(path: string, modelRegistry: ModelRegistry): void
|
|
|
144
144
|
resolvePrewarmImage(read.buf, read.mimeType, resizeImage)
|
|
145
145
|
.then((img) => {
|
|
146
146
|
if (!img) return;
|
|
147
|
+
// Paste happens while the agent is idle (no run → no live signal).
|
|
148
|
+
// Reset the turn-abort controller so a previous turn's ESC doesn't leave
|
|
149
|
+
// it aborted and short-circuit this prewarm; the next submit's
|
|
150
|
+
// `before_agent_start` resets again. The prewarm batch uses the loader's
|
|
151
|
+
// controller and becomes abortable once a run starts and binds a live
|
|
152
|
+
// signal.
|
|
153
|
+
loader.resetTurnAbort();
|
|
147
154
|
loader.setPendingTurnPrompt("");
|
|
148
155
|
loader.bindTurnContext({ modelRegistry });
|
|
149
156
|
loader.loadDescription(img).catch(() => {});
|
|
@@ -248,6 +255,18 @@ export default function (pi: ExtensionAPI) {
|
|
|
248
255
|
if (!isConfigured(config)) return;
|
|
249
256
|
if (!isHandoffTarget(ctx.model, config)) return;
|
|
250
257
|
|
|
258
|
+
// Fresh turn → fresh turn-abort controller. `before_agent_start` fires
|
|
259
|
+
// BEFORE the agent run starts, so `ctx.signal` is undefined here (the run's
|
|
260
|
+
// abort signal doesn't exist yet — it's created in `agent.prompt()` →
|
|
261
|
+
// `runWithLifecycle`, which runs AFTER this event). The prewarm below
|
|
262
|
+
// dispatches describer batches now, and they must be abortable once the
|
|
263
|
+
// run's live signal arrives — so reset the loader's turn-abort controller
|
|
264
|
+
// here, and let the later `tool_result`/`context` binds forward the live
|
|
265
|
+
// signal into it. Without this reset, a previous turn's ESC would leave
|
|
266
|
+
// the controller aborted and every dispatch would short-circuit to
|
|
267
|
+
// UNAVAILABLE.
|
|
268
|
+
loader.resetTurnAbort();
|
|
269
|
+
|
|
251
270
|
// Capture this turn's user prompt so every image in the turn — attached or
|
|
252
271
|
// read via the read tool — is described in the same request context.
|
|
253
272
|
loader.setPendingTurnPrompt(event.prompt || "");
|