@vanillagreen/pi-qol 1.4.3 → 1.5.0
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 +26 -1
- package/extensions/qol/budget-guard-runtime.ts +100 -0
- package/extensions/qol/budget-guard.ts +317 -0
- package/extensions/qol/compaction-handoff.ts +190 -0
- package/extensions/qol/compaction.ts +135 -20
- package/extensions/qol/constants.ts +23 -1
- package/extensions/qol/context-usage.ts +35 -0
- package/extensions/qol/editor.ts +13 -5
- package/extensions/qol/glyphs.ts +122 -0
- package/extensions/qol/session-search/component.ts +33 -11
- package/extensions/qol/status-message.ts +2 -1
- package/extensions/qol/statusline.ts +5 -4
- package/extensions/qol/transcript-risk.ts +28 -0
- package/extensions/qol.ts +37 -0
- package/package.json +73 -3
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// Transcript risk for /context: estimate serialized request payload size and
|
|
2
|
+
// warn before it crosses a char budget, even when the token count alone has
|
|
3
|
+
// not reached the context window. Keeps /context independent of compaction.ts
|
|
4
|
+
// (no summarizer/fs/artifact dependencies on the UI path).
|
|
5
|
+
|
|
6
|
+
import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
|
7
|
+
import { convertToLlm, serializeConversation } from "@earendil-works/pi-coding-agent";
|
|
8
|
+
import { evaluateTranscriptRisk, type TranscriptRiskResult } from "./budget-guard.js";
|
|
9
|
+
|
|
10
|
+
function stripThinking(messages: any[]): any[] {
|
|
11
|
+
return messages.map((message: any) => {
|
|
12
|
+
if (message.role !== "assistant" || !Array.isArray(message.content)) return message;
|
|
13
|
+
return { ...message, content: message.content.filter((part: any) => part?.type !== "thinking") };
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function transcriptRiskState(messages: AgentMessage[], threshold: number): TranscriptRiskResult {
|
|
18
|
+
if (!Array.isArray(messages) || messages.length === 0 || threshold <= 0) {
|
|
19
|
+
return { chars: 0, exceeded: false, messageCount: messages?.length ?? 0, threshold };
|
|
20
|
+
}
|
|
21
|
+
try {
|
|
22
|
+
const serialized = serializeConversation(stripThinking(convertToLlm(messages)));
|
|
23
|
+
return evaluateTranscriptRisk({ chars: serialized.length, messageCount: messages.length, threshold });
|
|
24
|
+
} catch (error) {
|
|
25
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
26
|
+
return evaluateTranscriptRisk({ chars: 0, error: message, messageCount: messages.length, threshold });
|
|
27
|
+
}
|
|
28
|
+
}
|
package/extensions/qol.ts
CHANGED
|
@@ -2,7 +2,9 @@ import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "@e
|
|
|
2
2
|
import type { TUI } from "@earendil-works/pi-tui";
|
|
3
3
|
import { criticalInfo, lastAssistantTextFromAgentEnd, needsDirection, taskStats } from "./qol/agent-end.js";
|
|
4
4
|
import { getQuestionService, readCavemanBridge, type QuestionOpenedEventLike } from "./qol/bridges.js";
|
|
5
|
+
import { BudgetGuardDriver } from "./qol/budget-guard-runtime.js";
|
|
5
6
|
import {
|
|
7
|
+
budgetGuardTrigger,
|
|
6
8
|
compactionTriggerReason,
|
|
7
9
|
handleQolBranchSummary,
|
|
8
10
|
handleQolCompaction,
|
|
@@ -257,6 +259,35 @@ export default function qol(pi: ExtensionAPI): void {
|
|
|
257
259
|
idleCompactionTimer = undefined;
|
|
258
260
|
};
|
|
259
261
|
|
|
262
|
+
const budgetGuardDriver = new BudgetGuardDriver();
|
|
263
|
+
|
|
264
|
+
const resetBudgetGuard = () => {
|
|
265
|
+
budgetGuardDriver.reset();
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
const maybeFireBudgetGuard = (ctx: ExtensionContext) => {
|
|
269
|
+
let trigger;
|
|
270
|
+
try {
|
|
271
|
+
trigger = budgetGuardTrigger(ctx);
|
|
272
|
+
} catch (error) {
|
|
273
|
+
if (isStaleCtxError(error)) return;
|
|
274
|
+
throw error;
|
|
275
|
+
}
|
|
276
|
+
const notifySafely = (message: string, level: "info" | "warning" | "error") => {
|
|
277
|
+
try {
|
|
278
|
+
if (ctx.hasUI && settingBoolean("compaction.notify", true, ctx.cwd)) ctx.ui.notify(message, level);
|
|
279
|
+
} catch (error) {
|
|
280
|
+
if (isStaleCtxError(error)) return;
|
|
281
|
+
throw error;
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
budgetGuardDriver.dispatch({
|
|
285
|
+
compact: typeof ctx.compact === "function" ? ctx.compact.bind(ctx) : undefined,
|
|
286
|
+
notify: notifySafely,
|
|
287
|
+
trigger,
|
|
288
|
+
});
|
|
289
|
+
};
|
|
290
|
+
|
|
260
291
|
const scheduleIdleCompaction = (ctx: ExtensionContext) => {
|
|
261
292
|
clearIdleCompactionTimer();
|
|
262
293
|
if (!settingBoolean("compaction.idleEnabled", false, ctx.cwd)) return;
|
|
@@ -509,6 +540,7 @@ export default function qol(pi: ExtensionAPI): void {
|
|
|
509
540
|
subscribeCavemanBridge();
|
|
510
541
|
latestSystemPromptOptions = undefined;
|
|
511
542
|
resetAutoRename();
|
|
543
|
+
resetBudgetGuard();
|
|
512
544
|
resetThinkingTimer(ctx);
|
|
513
545
|
void consumePendingSessionSearchContext(pi, ctx, event.reason);
|
|
514
546
|
installAutocompleteHintStyling(ctx);
|
|
@@ -569,6 +601,7 @@ export default function qol(pi: ExtensionAPI): void {
|
|
|
569
601
|
cavemanUnsubscribe?.();
|
|
570
602
|
cavemanUnsubscribe = undefined;
|
|
571
603
|
resetAutoRename();
|
|
604
|
+
resetBudgetGuard();
|
|
572
605
|
clearIdleCompactionTimer();
|
|
573
606
|
clearQuestionSubscribeTimer();
|
|
574
607
|
if (sessionSearchWarmupTimer) clearTimeout(sessionSearchWarmupTimer);
|
|
@@ -637,6 +670,7 @@ export default function qol(pi: ExtensionAPI): void {
|
|
|
637
670
|
void refreshStatusline(ctx);
|
|
638
671
|
requestRender();
|
|
639
672
|
}
|
|
673
|
+
maybeFireBudgetGuard(ctx);
|
|
640
674
|
scheduleIdleCompaction(ctx);
|
|
641
675
|
void attemptAutoRename(ctx);
|
|
642
676
|
const text = lastAssistantTextFromAgentEnd(event, ctx);
|
|
@@ -660,6 +694,9 @@ export default function qol(pi: ExtensionAPI): void {
|
|
|
660
694
|
sendQolNotification(ctx, "ready", settingString("notification.readyMessage", "Ready for input", ctx.cwd), "info", "ready");
|
|
661
695
|
});
|
|
662
696
|
pi.on("session_compact", (_event, ctx) => {
|
|
697
|
+
// After a successful compaction usage drops below the budget, so reset the
|
|
698
|
+
// crossing key so the next threshold crossing re-fires the guard.
|
|
699
|
+
budgetGuardDriver.noteSessionCompacted();
|
|
663
700
|
if (!ctx.hasUI) return;
|
|
664
701
|
void refreshStatusline(ctx);
|
|
665
702
|
requestRender();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vanillagreen/pi-qol",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "Pi quality-of-life extension: compact statusline/π prompt, reliable multiline input, styled pasted-image chips, session naming/search/context import, handoff, permission prompts, notifications, custom compaction, and a collapsed-thinking timer.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"keywords": [
|
|
@@ -16,6 +16,9 @@
|
|
|
16
16
|
],
|
|
17
17
|
"image": "https://raw.githubusercontent.com/vanillagreencom/vstack/main/pi-extensions/pi-qol/assets/settings-panel.png"
|
|
18
18
|
},
|
|
19
|
+
"scripts": {
|
|
20
|
+
"test": "bun test ./tests"
|
|
21
|
+
},
|
|
19
22
|
"vstack": {
|
|
20
23
|
"extensionManager": {
|
|
21
24
|
"displayName": "QOL",
|
|
@@ -30,6 +33,19 @@
|
|
|
30
33
|
"apply": "reload",
|
|
31
34
|
"requiresReload": true
|
|
32
35
|
},
|
|
36
|
+
{
|
|
37
|
+
"key": "glyphStyle",
|
|
38
|
+
"label": "Glyph style",
|
|
39
|
+
"description": "Glyphs for vstack-owned UI chrome. Unicode is the default; ASCII uses plain box/status/separator characters for terminal compatibility. pi-tool-renderer's global override wins when set.",
|
|
40
|
+
"type": "enum",
|
|
41
|
+
"enumValues": [
|
|
42
|
+
"unicode",
|
|
43
|
+
"ascii"
|
|
44
|
+
],
|
|
45
|
+
"default": "unicode",
|
|
46
|
+
"category": "Rendering",
|
|
47
|
+
"apply": "live"
|
|
48
|
+
},
|
|
33
49
|
{
|
|
34
50
|
"key": "replaceFooter",
|
|
35
51
|
"label": "Replace built-in footer",
|
|
@@ -654,9 +670,9 @@
|
|
|
654
670
|
{
|
|
655
671
|
"key": "compaction.model",
|
|
656
672
|
"label": "Compaction model",
|
|
657
|
-
"description": "Summarizer model as provider/model, a bare model id, or current. Default
|
|
673
|
+
"description": "Summarizer model as provider/model, a bare model id, or current. Default uses Pi's active model.",
|
|
658
674
|
"type": "string",
|
|
659
|
-
"default": "
|
|
675
|
+
"default": "current",
|
|
660
676
|
"category": "Compaction",
|
|
661
677
|
"apply": "live"
|
|
662
678
|
},
|
|
@@ -782,6 +798,60 @@
|
|
|
782
798
|
"category": "Compaction Threshold",
|
|
783
799
|
"apply": "live"
|
|
784
800
|
},
|
|
801
|
+
{
|
|
802
|
+
"key": "compaction.budgetGuardEnabled",
|
|
803
|
+
"label": "Long-session budget guard",
|
|
804
|
+
"description": "Trigger compaction on agent_end (no idle wait) when context usage crosses budgetPercent or budgetTokens. Designed for autonomous/Flightdeck runs so transcript growth cannot silently hit provider/buffer limits. Fires once per threshold crossing, not in a loop.",
|
|
805
|
+
"type": "boolean",
|
|
806
|
+
"default": true,
|
|
807
|
+
"category": "Compaction Budget Guard",
|
|
808
|
+
"apply": "live"
|
|
809
|
+
},
|
|
810
|
+
{
|
|
811
|
+
"key": "compaction.budgetPercent",
|
|
812
|
+
"label": "Budget guard percent",
|
|
813
|
+
"description": "Context-window percentage that fires the budget guard at agent_end. Set -1 to disable percent-based firing and rely on absolute tokens instead.",
|
|
814
|
+
"type": "number",
|
|
815
|
+
"default": 85,
|
|
816
|
+
"category": "Compaction Budget Guard",
|
|
817
|
+
"apply": "live"
|
|
818
|
+
},
|
|
819
|
+
{
|
|
820
|
+
"key": "compaction.budgetTokens",
|
|
821
|
+
"label": "Budget guard token limit",
|
|
822
|
+
"description": "Absolute token count above which the budget guard fires at agent_end. Set -1 to use only budgetPercent.",
|
|
823
|
+
"type": "number",
|
|
824
|
+
"default": -1,
|
|
825
|
+
"category": "Compaction Budget Guard",
|
|
826
|
+
"apply": "live"
|
|
827
|
+
},
|
|
828
|
+
{
|
|
829
|
+
"key": "compaction.maxInputChars",
|
|
830
|
+
"label": "Chunked compaction input cap",
|
|
831
|
+
"description": "Maximum serialized characters sent to the summarizer in a single request. Inputs larger than this are split into chunks, summarized chunk-by-chunk, then merged via a summary-of-summaries pass so the compaction request payload itself cannot blow past provider buffer limits. Set 0 to disable chunking.",
|
|
832
|
+
"type": "number",
|
|
833
|
+
"default": 240000,
|
|
834
|
+
"category": "Compaction Budget Guard",
|
|
835
|
+
"apply": "live"
|
|
836
|
+
},
|
|
837
|
+
{
|
|
838
|
+
"key": "compaction.handoffArtifactEnabled",
|
|
839
|
+
"label": "Write pre-compaction handoff artifact",
|
|
840
|
+
"description": "Before compaction, write a JSON handoff artifact under ~/.pi/agent/vstack/sessions/<session>/pi-qol/handoff/ containing the previous summary, last task state, and recently-referenced file/artifact paths. Always writes a stamped file and a latest.json pointer. Runs whenever the QOL bounded compaction handler runs — that is, on every budget-guard-triggered compaction (regardless of compaction.customEnabled), and on manual/idle compactions only when compaction.customEnabled is on. Write failures surface a QOL warning and a handoffArtifactError field in the compaction details.",
|
|
841
|
+
"type": "boolean",
|
|
842
|
+
"default": true,
|
|
843
|
+
"category": "Compaction Budget Guard",
|
|
844
|
+
"apply": "live"
|
|
845
|
+
},
|
|
846
|
+
{
|
|
847
|
+
"key": "compaction.transcriptRiskWarnChars",
|
|
848
|
+
"label": "Transcript-risk warn budget (chars)",
|
|
849
|
+
"description": "Show a transcript-risk warning in /context when the serialized payload of the messages-to-send exceeds this many characters, even if the token count is still below the model context window. Set 0 to disable the warning.",
|
|
850
|
+
"type": "number",
|
|
851
|
+
"default": 600000,
|
|
852
|
+
"category": "Compaction Budget Guard",
|
|
853
|
+
"apply": "live"
|
|
854
|
+
},
|
|
785
855
|
{
|
|
786
856
|
"key": "thinkingLabel.text",
|
|
787
857
|
"label": "Hidden thinking label",
|