bermudis-pi-goodies 0.23.0 → 0.23.2
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 +1 -1
- package/clean-tui.ts +94 -24
- package/goodies.ts +7 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -25,7 +25,7 @@ extensions. One entry point, twelve independent features.
|
|
|
25
25
|
After publishing the package to npm:
|
|
26
26
|
|
|
27
27
|
```bash
|
|
28
|
-
pi install npm:bermudis-pi-goodies@0.23.
|
|
28
|
+
pi install npm:bermudis-pi-goodies@0.23.2
|
|
29
29
|
```
|
|
30
30
|
|
|
31
31
|
Remove any old `bermudis-pi-goodies.ts` symlink before reloading Pi. Each
|
package/clean-tui.ts
CHANGED
|
@@ -44,7 +44,12 @@ import { Box, Container, Text } from "@earendil-works/pi-tui";
|
|
|
44
44
|
import { homedir } from "node:os";
|
|
45
45
|
import { readFileSync } from "node:fs";
|
|
46
46
|
import { completeSimple } from "@earendil-works/pi-ai/compat";
|
|
47
|
-
import type {
|
|
47
|
+
import type {
|
|
48
|
+
Api,
|
|
49
|
+
AssistantMessage,
|
|
50
|
+
Model,
|
|
51
|
+
ThinkingLevel,
|
|
52
|
+
} from "@earendil-works/pi-ai";
|
|
48
53
|
import { logGoodiesEvent, setGoodiesLogPathForTesting } from "./goodies-log.ts";
|
|
49
54
|
import { describeError } from "./json-file.ts";
|
|
50
55
|
import {
|
|
@@ -596,8 +601,8 @@ async function summarizeViaProvider(
|
|
|
596
601
|
signal: AbortSignal,
|
|
597
602
|
): Promise<string> {
|
|
598
603
|
const t = await resolveSummaryTransport();
|
|
599
|
-
const response = await
|
|
600
|
-
t
|
|
604
|
+
const response = await completeSummaryTurn(
|
|
605
|
+
t,
|
|
601
606
|
{
|
|
602
607
|
messages: [
|
|
603
608
|
{
|
|
@@ -609,13 +614,7 @@ async function summarizeViaProvider(
|
|
|
609
614
|
},
|
|
610
615
|
],
|
|
611
616
|
},
|
|
612
|
-
|
|
613
|
-
apiKey: t.apiKey,
|
|
614
|
-
headers: t.headers,
|
|
615
|
-
maxTokens: SUMMARY_MAX_TOKENS,
|
|
616
|
-
signal,
|
|
617
|
-
reasoning: summaryReasoning(t.model),
|
|
618
|
-
},
|
|
617
|
+
signal,
|
|
619
618
|
);
|
|
620
619
|
return convertSummaryResponse(response, t.label);
|
|
621
620
|
}
|
|
@@ -625,8 +624,8 @@ async function summarizeThinkingViaProvider(
|
|
|
625
624
|
signal: AbortSignal,
|
|
626
625
|
): Promise<string> {
|
|
627
626
|
const t = await resolveSummaryTransport();
|
|
628
|
-
const response = await
|
|
629
|
-
t
|
|
627
|
+
const response = await completeSummaryTurn(
|
|
628
|
+
t,
|
|
630
629
|
{
|
|
631
630
|
messages: [
|
|
632
631
|
{
|
|
@@ -636,13 +635,7 @@ async function summarizeThinkingViaProvider(
|
|
|
636
635
|
},
|
|
637
636
|
],
|
|
638
637
|
},
|
|
639
|
-
|
|
640
|
-
apiKey: t.apiKey,
|
|
641
|
-
headers: t.headers,
|
|
642
|
-
maxTokens: SUMMARY_MAX_TOKENS,
|
|
643
|
-
signal,
|
|
644
|
-
reasoning: summaryReasoning(t.model),
|
|
645
|
-
},
|
|
638
|
+
signal,
|
|
646
639
|
);
|
|
647
640
|
return convertSummaryResponse(response, t.label);
|
|
648
641
|
}
|
|
@@ -654,6 +647,25 @@ async function summarizeThinkingViaProvider(
|
|
|
654
647
|
* thing a raw endpoint could never do. Safe to call fire-and-forget
|
|
655
648
|
* (pi-codex makes OAuth-refreshing calls the same way).
|
|
656
649
|
*/
|
|
650
|
+
/**
|
|
651
|
+
* Summaries are best-effort: an unconfigured model (or a session whose model
|
|
652
|
+
* registry never arrived) means the feature is OFF, not failing. Those
|
|
653
|
+
* conditions throw this sentinel so every failure path can drop them
|
|
654
|
+
* silently — no log line, no backoff, no pause widget. Config can flip
|
|
655
|
+
* underneath a session at any moment (another pi session rewrites
|
|
656
|
+
* goodies.json on every write), so this must be checked at the failure
|
|
657
|
+
* boundary, not only at the enqueue gates.
|
|
658
|
+
*/
|
|
659
|
+
function summariesOffError(reason: string): Error {
|
|
660
|
+
const err = new Error(reason);
|
|
661
|
+
err.name = "SummariesOffError";
|
|
662
|
+
return err;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
function isSummariesOffError(err: unknown): boolean {
|
|
666
|
+
return (err as Error | undefined)?.name === "SummariesOffError";
|
|
667
|
+
}
|
|
668
|
+
|
|
657
669
|
async function resolveSummaryTransport(): Promise<{
|
|
658
670
|
model: Model<Api>;
|
|
659
671
|
label: string;
|
|
@@ -661,10 +673,10 @@ async function resolveSummaryTransport(): Promise<{
|
|
|
661
673
|
headers?: Record<string, string | null>;
|
|
662
674
|
}> {
|
|
663
675
|
const configured = getSummaryModel();
|
|
664
|
-
if (!configured) throw
|
|
676
|
+
if (!configured) throw summariesOffError("no summary model configured");
|
|
665
677
|
const registry = summaryModelRegistry;
|
|
666
678
|
if (!registry)
|
|
667
|
-
throw
|
|
679
|
+
throw summariesOffError("model registry not captured yet this session");
|
|
668
680
|
const found = findSummaryModel(registry, configured);
|
|
669
681
|
if (!found)
|
|
670
682
|
throw new Error(`summary model "${configured}" not found in registry`);
|
|
@@ -748,6 +760,46 @@ function summaryReasoning(model: Model<Api>): ThinkingLevel | undefined {
|
|
|
748
760
|
return "minimal";
|
|
749
761
|
}
|
|
750
762
|
|
|
763
|
+
/**
|
|
764
|
+
* Run one summary completion through pi-ai, shared by both summary kinds so
|
|
765
|
+
* the reasoning_effort compatibility retry lives in exactly one place.
|
|
766
|
+
*
|
|
767
|
+
* Some OpenAI-compatible endpoints validate reasoning_effort against their
|
|
768
|
+
* own enum and reject "minimal" outright — observed on Command Code
|
|
769
|
+
* (commandcode/poolside/*): 400 invalid_request_error, param:"reasoning_effort",
|
|
770
|
+
* accepted values low|medium|high|xhigh|max. "low" is the floor of every known
|
|
771
|
+
* enum, so retry once there before surfacing the failure; an endpoint that
|
|
772
|
+
* rejects "low" too would pause as before.
|
|
773
|
+
*/
|
|
774
|
+
async function completeSummaryTurn(
|
|
775
|
+
t: Awaited<ReturnType<typeof resolveSummaryTransport>>,
|
|
776
|
+
context: Parameters<typeof completeSimple>[1],
|
|
777
|
+
signal: AbortSignal,
|
|
778
|
+
): Promise<AssistantMessage> {
|
|
779
|
+
const options = (reasoning: ThinkingLevel | undefined) => ({
|
|
780
|
+
apiKey: t.apiKey,
|
|
781
|
+
headers: t.headers,
|
|
782
|
+
maxTokens: SUMMARY_MAX_TOKENS,
|
|
783
|
+
signal,
|
|
784
|
+
reasoning,
|
|
785
|
+
});
|
|
786
|
+
// completeSimple does NOT throw for HTTP errors — it returns an
|
|
787
|
+
// AssistantMessage with stopReason:"error" + errorMessage, so the
|
|
788
|
+
// compatibility check inspects the response, not a catch block.
|
|
789
|
+
const response = await completeSimple(
|
|
790
|
+
t.model,
|
|
791
|
+
context,
|
|
792
|
+
options(summaryReasoning(t.model)),
|
|
793
|
+
);
|
|
794
|
+
if (
|
|
795
|
+
response.stopReason === "error" &&
|
|
796
|
+
(response.errorMessage ?? "").includes("reasoning_effort")
|
|
797
|
+
) {
|
|
798
|
+
return await completeSimple(t.model, context, options("low"));
|
|
799
|
+
}
|
|
800
|
+
return response;
|
|
801
|
+
}
|
|
802
|
+
|
|
751
803
|
// Summaries are best-effort polish over the heuristic hint, but failures must
|
|
752
804
|
// not be silent: every request lands in the structured log with its outcome,
|
|
753
805
|
// so request volume and a broken provider/key/model choice are queryable
|
|
@@ -935,8 +987,14 @@ function startSummaryRequest(cmd: string): void {
|
|
|
935
987
|
// concurrency slot with zero log output.
|
|
936
988
|
if (!signal.aborted) pendingSummaries.delete(cmd);
|
|
937
989
|
// Switching sessions aborts in-flight summaries deliberately: that is
|
|
938
|
-
// not a provider failure — neither penalize nor log it.
|
|
939
|
-
|
|
990
|
+
// not a provider failure — neither penalize nor log it. Same for the
|
|
991
|
+
// feature being switched off underneath the request (config rewrite by
|
|
992
|
+
// another session): drop silently, no backoff, no pause widget.
|
|
993
|
+
if (
|
|
994
|
+
signal.aborted ||
|
|
995
|
+
(result.err as Error)?.name === "AbortError" ||
|
|
996
|
+
isSummariesOffError(result.err)
|
|
997
|
+
)
|
|
940
998
|
return;
|
|
941
999
|
const pauseMs = noteSummaryFailure(result.err);
|
|
942
1000
|
logSummaryFailure(
|
|
@@ -1069,6 +1127,14 @@ async function summarizeWithRetries(job: {
|
|
|
1069
1127
|
|
|
1070
1128
|
/** Start queued requests while capacity allows and no backoff is active. */
|
|
1071
1129
|
function drainSummaryQueue(): void {
|
|
1130
|
+
// The feature can be switched off between enqueue and drain — config is
|
|
1131
|
+
// re-read from disk on every write and any pi session can rewrite it. Off
|
|
1132
|
+
// means off: drop the deferred requests instead of draining them into
|
|
1133
|
+
// requests that resolveSummaryTransport can only refuse.
|
|
1134
|
+
if (!getSummaryModel()) {
|
|
1135
|
+
summaryRequestQueue.length = 0;
|
|
1136
|
+
return;
|
|
1137
|
+
}
|
|
1072
1138
|
while (
|
|
1073
1139
|
summaryRequestQueue.length > 0 &&
|
|
1074
1140
|
pendingSummaries.size < SUMMARY_MAX_INFLIGHT &&
|
|
@@ -1321,7 +1387,11 @@ function maybeRequestThinkingSummary(text: string): void {
|
|
|
1321
1387
|
}
|
|
1322
1388
|
return;
|
|
1323
1389
|
}
|
|
1324
|
-
if (
|
|
1390
|
+
if (
|
|
1391
|
+
!signal.aborted &&
|
|
1392
|
+
(result.err as Error)?.name !== "AbortError" &&
|
|
1393
|
+
!isSummariesOffError(result.err)
|
|
1394
|
+
) {
|
|
1325
1395
|
const pauseMs = noteSummaryFailure(result.err);
|
|
1326
1396
|
logSummaryFailure(
|
|
1327
1397
|
text,
|
package/goodies.ts
CHANGED
|
@@ -341,13 +341,18 @@ export function completeGoodiesArguments(
|
|
|
341
341
|
if (verb === "summary-model") {
|
|
342
342
|
// Single value token (model ids contain no spaces): "off"/"default"
|
|
343
343
|
// clear the setting; anything else matches the catalogue, ranked.
|
|
344
|
+
// Values MUST carry the verb: pi applies a selection by replacing the
|
|
345
|
+
// whole argument-text span with item.value (both its slash-argument
|
|
346
|
+
// path and the forced-Tab wrapper use prefix: argumentText), so bare
|
|
347
|
+
// values wipe "summary-model" off the line. enable/disable and
|
|
348
|
+
// thinking-summaries already follow this pattern.
|
|
344
349
|
const q = valuePrefix.toLowerCase();
|
|
345
350
|
const items: AutocompleteItem[] = [];
|
|
346
351
|
for (const w of ["off", "default"]) {
|
|
347
|
-
if (w.startsWith(q)) items.push({ value: w
|
|
352
|
+
if (w.startsWith(q)) items.push({ value: `${verb} ${w}`, label: w });
|
|
348
353
|
}
|
|
349
354
|
for (const c of rankCandidates(completionModels, q).slice(0, 20)) {
|
|
350
|
-
items.push({ value: c
|
|
355
|
+
items.push({ value: `${verb} ${c}`, label: c });
|
|
351
356
|
}
|
|
352
357
|
return items.length ? items : null;
|
|
353
358
|
}
|