pi-better-harness 0.40.3 → 0.40.5
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.
|
@@ -278,6 +278,19 @@ function sdkEntry(require: ReturnType<typeof createRequire>): string {
|
|
|
278
278
|
}
|
|
279
279
|
|
|
280
280
|
/** Internal TUI adapter: no tools are replaced and no result content is changed. */
|
|
281
|
+
const FRAME_MS = 80;
|
|
282
|
+
const IDLE_FRAME_MS = 1000;
|
|
283
|
+
const STALE_RENDER_MS = 400;
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Shimmer cadence for the next frame. Fast while any animated row rendered recently, one frame
|
|
287
|
+
* a second once none has, and never off: an unfocused window can stop repainting without the row
|
|
288
|
+
* being gone, and stopping the timer would latch the animation until an unrelated render restarted it.
|
|
289
|
+
*/
|
|
290
|
+
export function shimmerFrameDelay(lastRenderMs: number, nowMs: number): number {
|
|
291
|
+
return nowMs - lastRenderMs <= STALE_RENDER_MS ? FRAME_MS : IDLE_FRAME_MS;
|
|
292
|
+
}
|
|
293
|
+
|
|
281
294
|
export function installMinimalOutputHook(prototype: ToolPrototype, getTheme?: () => Theme, compactionPrototype?: CompactionPrototype, customMessagePrototype?: CustomMessagePrototype): MinimalOutputHook {
|
|
282
295
|
for (const name of ["updateDisplay", "getResultRenderer", "getTextOutput", "setExpanded", "invalidate", "render"] as const) {
|
|
283
296
|
if (typeof prototype[name] !== "function") {
|
|
@@ -319,10 +332,16 @@ export function installMinimalOutputHook(prototype: ToolPrototype, getTheme?: ()
|
|
|
319
332
|
const animationReferences = new WeakMap<ToolComponent, WeakRef<ToolComponent>>();
|
|
320
333
|
const animated = new Map<WeakRef<ToolComponent>, number>();
|
|
321
334
|
const settled = new WeakSet<ToolComponent>();
|
|
322
|
-
|
|
335
|
+
// Frame while the transcript paints, then one frame a second once it stops. An unfocused
|
|
336
|
+
// window may stop repainting, and clearing the timer outright would latch the animation off
|
|
337
|
+
// until an unrelated render happened to restart it. Slowing keeps the saving without the latch.
|
|
338
|
+
let animationTimer: ReturnType<typeof setTimeout> | undefined;
|
|
339
|
+
let animationIdle = false;
|
|
340
|
+
let paintingFrame = false;
|
|
323
341
|
const stopAnimation = () => {
|
|
324
|
-
if (animationTimer !== undefined)
|
|
342
|
+
if (animationTimer !== undefined) clearTimeout(animationTimer);
|
|
325
343
|
animationTimer = undefined;
|
|
344
|
+
animationIdle = false;
|
|
326
345
|
animated.clear();
|
|
327
346
|
};
|
|
328
347
|
const canAnimate = (component: ToolComponent) => {
|
|
@@ -333,30 +352,57 @@ export function installMinimalOutputHook(prototype: ToolPrototype, getTheme?: ()
|
|
|
333
352
|
&& !settled.has(component);
|
|
334
353
|
};
|
|
335
354
|
const pruneAnimation = () => {
|
|
336
|
-
for (const
|
|
355
|
+
for (const reference of animated.keys()) {
|
|
337
356
|
const component = reference.deref();
|
|
338
|
-
if (!component || !canAnimate(component)
|
|
357
|
+
if (!component || !canAnimate(component)
|
|
339
358
|
|| toolVisibility(component.ui, component) !== true) animated.delete(reference);
|
|
340
359
|
}
|
|
341
360
|
if (!animated.size) stopAnimation();
|
|
342
361
|
};
|
|
362
|
+
const scheduleFrame = (delayMs: number) => {
|
|
363
|
+
if (animationTimer !== undefined) clearTimeout(animationTimer);
|
|
364
|
+
animationTimer = setTimeout(paintFrame, Math.max(0, delayMs));
|
|
365
|
+
animationTimer.unref?.();
|
|
366
|
+
};
|
|
367
|
+
const paintFrame = () => {
|
|
368
|
+
animationTimer = undefined;
|
|
369
|
+
pruneAnimation();
|
|
370
|
+
if (!animated.size) return;
|
|
371
|
+
const uis = new Set<ToolUI>();
|
|
372
|
+
for (const reference of animated.keys()) {
|
|
373
|
+
const component = reference.deref();
|
|
374
|
+
if (!component) continue;
|
|
375
|
+
uis.add(component.ui);
|
|
376
|
+
}
|
|
377
|
+
paintingFrame = true;
|
|
378
|
+
try {
|
|
379
|
+
for (const ui of uis) ui.requestRender();
|
|
380
|
+
} finally {
|
|
381
|
+
paintingFrame = false;
|
|
382
|
+
}
|
|
383
|
+
pruneAnimation();
|
|
384
|
+
if (!animated.size) return;
|
|
385
|
+
// Choose the cadence from the timestamps this frame produced. Reading them before the
|
|
386
|
+
// renders instead would keep seeing the previous idle gap and re-arm the idle period
|
|
387
|
+
// forever, so painting that resumed on a timer-driven frame could never speed back up.
|
|
388
|
+
let newestRender = 0;
|
|
389
|
+
for (const lastRender of animated.values()) {
|
|
390
|
+
if (lastRender > newestRender) newestRender = lastRender;
|
|
391
|
+
}
|
|
392
|
+
const delay = shimmerFrameDelay(newestRender, Date.now());
|
|
393
|
+
animationIdle = delay === IDLE_FRAME_MS;
|
|
394
|
+
scheduleFrame(delay);
|
|
395
|
+
};
|
|
343
396
|
const animationTime = (component: ToolComponent, width: number, indent: number) => {
|
|
344
397
|
if (width <= indent + 3 || !canAnimate(component) || toolVisibility(component.ui, component) === false) return undefined;
|
|
345
398
|
let reference = animationReferences.get(component);
|
|
346
399
|
if (!reference) { reference = new WeakRef(component); animationReferences.set(component, reference); }
|
|
347
400
|
animated.set(reference, Date.now());
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
const component = reference.deref();
|
|
354
|
-
if (!component) continue;
|
|
355
|
-
uis.add(component.ui);
|
|
356
|
-
}
|
|
357
|
-
for (const ui of uis) ui.requestRender();
|
|
358
|
-
}, 80);
|
|
359
|
-
animationTimer.unref?.();
|
|
401
|
+
// A render outside the clock means painting resumed. Re-arm at the frame cadence rather
|
|
402
|
+
// than waiting out a pending idle period, which would otherwise cost up to a second.
|
|
403
|
+
if (!paintingFrame && (animationTimer === undefined || animationIdle)) {
|
|
404
|
+
animationIdle = false;
|
|
405
|
+
scheduleFrame(FRAME_MS);
|
|
360
406
|
}
|
|
361
407
|
return toolVisibility(component.ui, component) === true ? Date.now() : undefined;
|
|
362
408
|
};
|
|
@@ -108,7 +108,9 @@ DeepSeek also excludes Chinese public holidays. The bundled holiday list is curr
|
|
|
108
108
|
- When every candidate is excluded but at least one failed transiently, retry the model the turn already uses instead of ending the turn. Only rejected requests can end it with no route left.
|
|
109
109
|
- Preserve the failed model only after a context overflow, because Pi retries that request after compacting the conversation and a switch would pay a cold cache.
|
|
110
110
|
- Report excluded and demoted providers as router state only when those lists change, so a long session does not append one state entry per request.
|
|
111
|
-
- Declare the virtual model's
|
|
111
|
+
- Declare the virtual model's context limits as a pair that fits the provider window. Pi reads them whenever a response names no physical model, which is what the router's own routing error looks like, and an undeclared limit counts as zero. A zero window makes every conversation look over the compaction threshold, so Pi would compact after each routing error at any context size.
|
|
112
|
+
|
|
113
|
+
Both candidates share one 1,048,576-token window between input and output, but Pi treats `contextWindow` as the input allowance and `maxTokens` as room beyond it. The router therefore declares an input budget of `1,048,576 - 64,000` beside a 64,000-token output budget, so the two add up to the provider window. Declaring the raw 1,048,576 window beside a large output budget promises capacity no endpoint has: a 746,917-token conversation plus a 384,000-token completion is 1,130,917 tokens, which the provider rejects. The same pair also keeps Pi's compaction trigger below the point where output stops fitting.
|
|
112
114
|
- Stop with an error when every candidate is unavailable or rejected. A new user turn clears the exclusions.
|
|
113
115
|
- Direct requests, such as compaction summaries, compare prices independently.
|
|
114
116
|
|
|
@@ -22,12 +22,20 @@ export interface VirtualDefinition {
|
|
|
22
22
|
contextWindow: number; maxTokens: number;
|
|
23
23
|
route(request: RouteRequest, ctx: ExtensionContext): RouteResult;
|
|
24
24
|
}
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
// router's own routing error: with no declared limit Pi falls back to a zero window, sees
|
|
25
|
+
// Pi needs a declared window whenever a response names no physical model, which is exactly
|
|
26
|
+
// the router's own routing error: with no declared limit Pi falls back to a zero window, sees
|
|
28
27
|
// every conversation as over the threshold, and compacts after each routing error.
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
//
|
|
29
|
+
// Both candidate families share one 1,048,576-token window between input and output, but Pi
|
|
30
|
+
// treats contextWindow as the input allowance and maxTokens as room beyond it. Declaring the
|
|
31
|
+
// raw window beside a large output budget therefore promises capacity the provider does not
|
|
32
|
+
// have: 746,917 tokens of history plus a 384,000-token completion is 1,130,917, and the
|
|
33
|
+
// request is rejected for exceeding 1,048,576. Declare the input budget the shared window
|
|
34
|
+
// leaves after the output budget instead, so both the compaction threshold and the
|
|
35
|
+
// request-time clamp stay inside the real window.
|
|
36
|
+
const PROVIDER_CONTEXT_WINDOW = 1_048_576;
|
|
37
|
+
const MAX_TOKENS = 64_000;
|
|
38
|
+
const CONTEXT_WINDOW = PROVIDER_CONTEXT_WINDOW - MAX_TOKENS;
|
|
31
39
|
function sameList(left: readonly string[] = [], right: readonly string[] = []): boolean {
|
|
32
40
|
return left.length === right.length && left.every((value, index) => value === right[index]);
|
|
33
41
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-better-harness",
|
|
3
|
-
"version": "0.40.
|
|
3
|
+
"version": "0.40.5",
|
|
4
4
|
"description": "Pi extension bundle for a write sandbox, subagents, background tasks, SSH, goals, model routing, and Outcome & Todo.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
"dependencies": {
|
|
58
58
|
"pi-better-background-tasks": "0.14.2",
|
|
59
59
|
"pi-better-goal": "0.12.1",
|
|
60
|
-
"pi-better-model-router": "0.1.
|
|
60
|
+
"pi-better-model-router": "0.1.4",
|
|
61
61
|
"pi-ollama-cloud": "0.13.1",
|
|
62
62
|
"pi-better-sandbox": "0.14.1",
|
|
63
63
|
"pi-better-ssh": "0.1.4",
|