@diegopetrucci/pi-extensions 0.1.44 → 0.1.46
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.
|
@@ -629,20 +629,56 @@ function buildUserPrompt(query: string, repos: string[], owners: string[], maxSe
|
|
|
629
629
|
return `Task: locate and cite exact GitHub code locations that answer the query.\n\nQuery: ${query}\nRepository filters: ${repos.length ? repos.join(", ") : "(none)"}\nOwner filters: ${owners.length ? owners.join(", ") : "(none)"}\nMax search results per gh search call: ${maxSearchResults}\nLocal checkout cache: ${cache.mode === "enabled" ? `enabled at ${cache.root}` : "disabled"}\nCache decision: ${cache.decisionReason}\n\nRespond directly with concise, citation-heavy findings. Always pass --limit ${maxSearchResults} to gh search code unless a narrower command is clearly better.`;
|
|
630
630
|
}
|
|
631
631
|
|
|
632
|
-
|
|
632
|
+
type AssistantLikeMessage = {
|
|
633
|
+
role?: string;
|
|
634
|
+
content?: unknown;
|
|
635
|
+
stopReason?: unknown;
|
|
636
|
+
errorMessage?: unknown;
|
|
637
|
+
};
|
|
638
|
+
|
|
639
|
+
function getLastAssistantMessage(messages: unknown[]): AssistantLikeMessage | undefined {
|
|
633
640
|
for (let i = messages.length - 1; i >= 0; i--) {
|
|
634
|
-
const message = messages[i] as
|
|
635
|
-
if (message?.role
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
641
|
+
const message = messages[i] as AssistantLikeMessage;
|
|
642
|
+
if (message?.role === "assistant") return message;
|
|
643
|
+
}
|
|
644
|
+
return undefined;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
function extractLastAssistantText(messages: unknown[]): string {
|
|
648
|
+
const message = getLastAssistantMessage(messages);
|
|
649
|
+
if (!message || !Array.isArray(message.content)) return "";
|
|
650
|
+
const parts: string[] = [];
|
|
651
|
+
for (const part of message.content) {
|
|
652
|
+
if (part && typeof part === "object" && (part as { type?: string }).type === "text") {
|
|
653
|
+
const text = (part as { text?: unknown }).text;
|
|
654
|
+
if (typeof text === "string") parts.push(text);
|
|
642
655
|
}
|
|
643
|
-
if (parts.length) return parts.join("").trim();
|
|
644
656
|
}
|
|
645
|
-
return "";
|
|
657
|
+
return parts.join("").trim();
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
function describeNoAnswerReason(message: AssistantLikeMessage | undefined, turns: number): string {
|
|
661
|
+
if (!message) return "the internal subagent produced no assistant message";
|
|
662
|
+
if (message.stopReason === "error") {
|
|
663
|
+
const errorMessage = typeof message.errorMessage === "string" && message.errorMessage.trim()
|
|
664
|
+
? message.errorMessage.trim()
|
|
665
|
+
: "provider/model error";
|
|
666
|
+
return `the internal subagent stopped with an error: ${errorMessage}`;
|
|
667
|
+
}
|
|
668
|
+
if (message.stopReason === "aborted") return "the internal subagent was aborted before producing an answer";
|
|
669
|
+
if (turns >= MAX_TURNS) return `the internal subagent reached the ${MAX_TURNS}-turn budget without producing a final answer`;
|
|
670
|
+
const stopReason = typeof message.stopReason === "string" && message.stopReason.trim()
|
|
671
|
+
? ` (stopReason: ${message.stopReason.trim()})`
|
|
672
|
+
: "";
|
|
673
|
+
return `the internal subagent completed without final assistant text${stopReason}`;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
function formatInternalFailure(details: LibrarianDetails, reason: string): string {
|
|
677
|
+
return [
|
|
678
|
+
`Internal librarian run failed: ${reason}.`,
|
|
679
|
+
`Diagnostics: status=${details.status}; model=${details.model.modelRef}; thinking=${details.model.thinkingLevel}; modelSelection=${details.model.autoSelected ? "auto" : "configured"}; turns=${details.turns}; toolCalls=${details.toolCalls.length}.`,
|
|
680
|
+
"No answer was produced. This is not a reliable \"no results found\" signal; retry later or configure a different Librarian model with /librarian-config.",
|
|
681
|
+
].join("\n");
|
|
646
682
|
}
|
|
647
683
|
|
|
648
684
|
function formatToolCall(call: ToolCall): string {
|
|
@@ -847,6 +883,13 @@ export default function librarianExtension(pi: ExtensionAPI) {
|
|
|
847
883
|
},
|
|
848
884
|
});
|
|
849
885
|
|
|
886
|
+
pi.on("tool_result", async (event) => {
|
|
887
|
+
if (event.toolName !== "librarian") return undefined;
|
|
888
|
+
const details = event.details as LibrarianDetails | undefined;
|
|
889
|
+
if (details?.status !== "error") return undefined;
|
|
890
|
+
return { isError: true };
|
|
891
|
+
});
|
|
892
|
+
|
|
850
893
|
pi.registerTool({
|
|
851
894
|
name: "librarian",
|
|
852
895
|
label: "Librarian",
|
|
@@ -1034,9 +1077,20 @@ export default function librarianExtension(pi: ExtensionAPI) {
|
|
|
1034
1077
|
await Promise.race([promptPromise, timeoutPromise]);
|
|
1035
1078
|
}
|
|
1036
1079
|
|
|
1080
|
+
const lastAssistant = session ? getLastAssistantMessage(session.state.messages) : undefined;
|
|
1037
1081
|
const answer = session ? extractLastAssistantText(session.state.messages) : "";
|
|
1038
|
-
|
|
1039
|
-
|
|
1082
|
+
if (answer) {
|
|
1083
|
+
lastContent = answer;
|
|
1084
|
+
details.status = "done";
|
|
1085
|
+
} else if (aborted) {
|
|
1086
|
+
lastContent = "Aborted";
|
|
1087
|
+
details.status = "aborted";
|
|
1088
|
+
} else {
|
|
1089
|
+
details.status = "error";
|
|
1090
|
+
const reason = describeNoAnswerReason(lastAssistant, details.turns);
|
|
1091
|
+
lastContent = formatInternalFailure(details, reason);
|
|
1092
|
+
details.error = reason;
|
|
1093
|
+
}
|
|
1040
1094
|
details.endedAt = Date.now();
|
|
1041
1095
|
emit(true);
|
|
1042
1096
|
|
|
@@ -27,7 +27,7 @@ By default, the extension:
|
|
|
27
27
|
4. tries a provider-specific hardcoded priority list first
|
|
28
28
|
5. falls back to a heuristic that favors stronger tiers like `opus`, `pro`, newer versions, and penalizes `mini`, `flash`, `haiku`, `spark`, etc.
|
|
29
29
|
|
|
30
|
-
The hardcoded rankings now cover pi's built-in provider set, including Together; see the provider matrix for the current provider-by-provider top picks.
|
|
30
|
+
The hardcoded rankings now cover pi's built-in provider set, including Claude Fable on Anthropic-compatible providers and Together; see the provider matrix for the current provider-by-provider top picks.
|
|
31
31
|
|
|
32
32
|
If no reasoning model exists on the current provider, it falls back to the best available model on that provider.
|
|
33
33
|
|
|
@@ -75,6 +75,7 @@ const ORACLE_CONFIG_FILE = "oracle.json";
|
|
|
75
75
|
|
|
76
76
|
const PROVIDER_MODEL_PREFERENCES: Record<string, string[]> = {
|
|
77
77
|
"amazon-bedrock": [
|
|
78
|
+
"claude-fable-5",
|
|
78
79
|
"claude-opus-4-8",
|
|
79
80
|
"claude-opus-4-7",
|
|
80
81
|
"claude-opus-4-6",
|
|
@@ -92,6 +93,7 @@ const PROVIDER_MODEL_PREFERENCES: Record<string, string[]> = {
|
|
|
92
93
|
"zai.glm-5",
|
|
93
94
|
],
|
|
94
95
|
anthropic: [
|
|
96
|
+
"claude-fable-5",
|
|
95
97
|
"claude-opus-4-8",
|
|
96
98
|
"claude-opus-4.8",
|
|
97
99
|
"claude-opus-4-7",
|
|
@@ -132,6 +134,7 @@ const PROVIDER_MODEL_PREFERENCES: Record<string, string[]> = {
|
|
|
132
134
|
],
|
|
133
135
|
cerebras: ["gpt-oss-120b", "zai-glm-4.7", "llama3.1-8b"],
|
|
134
136
|
"cloudflare-ai-gateway": [
|
|
137
|
+
"claude-fable-5",
|
|
135
138
|
"claude-opus-4-7",
|
|
136
139
|
"claude-opus-4-6",
|
|
137
140
|
"claude-opus-4-5",
|
|
@@ -159,6 +162,7 @@ const PROVIDER_MODEL_PREFERENCES: Record<string, string[]> = {
|
|
|
159
162
|
"accounts/fireworks/models/gpt-oss-120b",
|
|
160
163
|
],
|
|
161
164
|
"github-copilot": [
|
|
165
|
+
"claude-fable-5",
|
|
162
166
|
"claude-opus-4.8",
|
|
163
167
|
"claude-opus-4.7",
|
|
164
168
|
"claude-opus-4.6",
|
|
@@ -257,6 +261,7 @@ const PROVIDER_MODEL_PREFERENCES: Record<string, string[]> = {
|
|
|
257
261
|
"big-pickle",
|
|
258
262
|
],
|
|
259
263
|
opencode: [
|
|
264
|
+
"claude-fable-5",
|
|
260
265
|
"gpt-5.5-pro",
|
|
261
266
|
"gpt-5.5",
|
|
262
267
|
"gpt-5.4-pro",
|
|
@@ -289,6 +294,8 @@ const PROVIDER_MODEL_PREFERENCES: Record<string, string[]> = {
|
|
|
289
294
|
"kimi-k2.5",
|
|
290
295
|
],
|
|
291
296
|
openrouter: [
|
|
297
|
+
"anthropic/claude-fable-5",
|
|
298
|
+
"~anthropic/claude-fable-latest",
|
|
292
299
|
"anthropic/claude-opus-4.8",
|
|
293
300
|
"anthropic/claude-opus-4.8-fast",
|
|
294
301
|
"anthropic/claude-opus-4.7",
|
|
@@ -327,6 +334,7 @@ const PROVIDER_MODEL_PREFERENCES: Record<string, string[]> = {
|
|
|
327
334
|
"MiniMaxAI/MiniMax-M2.5",
|
|
328
335
|
],
|
|
329
336
|
"vercel-ai-gateway": [
|
|
337
|
+
"anthropic/claude-fable-5",
|
|
330
338
|
"anthropic/claude-opus-4.8",
|
|
331
339
|
"anthropic/claude-opus-4.7",
|
|
332
340
|
"anthropic/claude-opus-4.6",
|
|
@@ -549,6 +557,26 @@ function extractTextFromContent(content: unknown): string {
|
|
|
549
557
|
return parts.join("\n\n").trim();
|
|
550
558
|
}
|
|
551
559
|
|
|
560
|
+
// Errors that typically resolve on retry: provider overload, rate limiting,
|
|
561
|
+
// transient 5xx, gateway/network failures, and request timeouts. Keep this list
|
|
562
|
+
// pattern-based so we recognize variants across providers without enumerating them.
|
|
563
|
+
const TRANSIENT_ERROR_PATTERN =
|
|
564
|
+
/\b(overload(?:ed)?|rate[ _-]?limit(?:ed)?|too many requests|429|500|502|503|504|bad gateway|service unavailable|gateway timeout|temporarily unavailable|timed?[ _-]?out|timeout|econnreset|econnrefused|etimedout|enetunreach|socket hang up|fetch failed)\b/i;
|
|
565
|
+
|
|
566
|
+
function isTransientErrorMessage(message: string): boolean {
|
|
567
|
+
return TRANSIENT_ERROR_PATTERN.test(message);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
function formatOracleModelError(stopReason: "error" | "aborted", errorMessage: string | undefined): string {
|
|
571
|
+
const trimmed = errorMessage?.trim();
|
|
572
|
+
if (stopReason === "aborted") {
|
|
573
|
+
return trimmed ? `Oracle model turn aborted: ${trimmed}` : "Oracle model turn aborted.";
|
|
574
|
+
}
|
|
575
|
+
if (!trimmed) return "Oracle model error (no detail provided by provider).";
|
|
576
|
+
const base = `Oracle model error: ${trimmed}`;
|
|
577
|
+
return isTransientErrorMessage(trimmed) ? `${base} (transient; retry may succeed)` : base;
|
|
578
|
+
}
|
|
579
|
+
|
|
552
580
|
function parseVersionScore(text: string): number {
|
|
553
581
|
const matches = text.match(/\d+(?:\.\d+){0,2}/g) ?? [];
|
|
554
582
|
let best = 0;
|
|
@@ -809,6 +837,9 @@ async function runOracle(
|
|
|
809
837
|
let finalOutput = "";
|
|
810
838
|
let stderr = "";
|
|
811
839
|
|
|
840
|
+
let lastStopReason: string | undefined;
|
|
841
|
+
let lastErrorMessage: string | undefined;
|
|
842
|
+
|
|
812
843
|
const details: OracleDetails = {
|
|
813
844
|
...selection,
|
|
814
845
|
includeBash,
|
|
@@ -880,6 +911,11 @@ async function runOracle(
|
|
|
880
911
|
if (text) finalOutput = text;
|
|
881
912
|
currentText = "";
|
|
882
913
|
|
|
914
|
+
const stopReason = event.message.stopReason;
|
|
915
|
+
lastStopReason = typeof stopReason === "string" ? stopReason : undefined;
|
|
916
|
+
const errorMessageField = event.message.errorMessage;
|
|
917
|
+
lastErrorMessage = typeof errorMessageField === "string" ? errorMessageField : undefined;
|
|
918
|
+
|
|
883
919
|
const messageUsage = event.message.usage;
|
|
884
920
|
if (messageUsage) {
|
|
885
921
|
usage.turns += 1;
|
|
@@ -938,6 +974,19 @@ async function runOracle(
|
|
|
938
974
|
return { ok: false, error: "Oracle was aborted.", details };
|
|
939
975
|
}
|
|
940
976
|
|
|
977
|
+
// The pi subprocess in `--mode json` does not promote an errored assistant
|
|
978
|
+
// turn to a non-zero exit code or stderr; the error is only carried on the
|
|
979
|
+
// streamed assistant message via stopReason/errorMessage. Surface that here
|
|
980
|
+
// so callers can distinguish transient provider errors from a genuinely
|
|
981
|
+
// empty response.
|
|
982
|
+
if (lastStopReason === "error" || lastStopReason === "aborted") {
|
|
983
|
+
return {
|
|
984
|
+
ok: false,
|
|
985
|
+
error: formatOracleModelError(lastStopReason, lastErrorMessage),
|
|
986
|
+
details,
|
|
987
|
+
};
|
|
988
|
+
}
|
|
989
|
+
|
|
941
990
|
if (exitCode !== 0) {
|
|
942
991
|
return {
|
|
943
992
|
ok: false,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@diegopetrucci/pi-extensions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.46",
|
|
4
4
|
"description": "A collection of pi extensions and skills for annotation UIs, context management, workflow audits, review-comment triage, notifications, brrr push alerts, safety guards, GitHub research, repo-local knowledge, todos, tool rendering, model/provider helpers, and Xiaohei-style article illustrations.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
@@ -13,6 +13,11 @@
|
|
|
13
13
|
"type": "git",
|
|
14
14
|
"url": "git+https://github.com/diegopetrucci/pi-extensions.git"
|
|
15
15
|
},
|
|
16
|
+
"scripts": {
|
|
17
|
+
"ci": "npm run preflight:install-state && npm run typecheck",
|
|
18
|
+
"preflight:install-state": "node scripts/check-install-state.mjs",
|
|
19
|
+
"typecheck": "npx tsc --noEmit --module NodeNext --moduleResolution NodeNext --target ES2022 --skipLibCheck $(git ls-files '*.ts')"
|
|
20
|
+
},
|
|
16
21
|
"files": [
|
|
17
22
|
"extensions",
|
|
18
23
|
"assets",
|