@diegopetrucci/pi-extensions 0.1.45 → 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
- function extractLastAssistantText(messages: unknown[]): string {
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 { role?: string; content?: unknown };
635
- if (message?.role !== "assistant" || !Array.isArray(message.content)) continue;
636
- const parts: string[] = [];
637
- for (const part of message.content) {
638
- if (part && typeof part === "object" && (part as { type?: string }).type === "text") {
639
- const text = (part as { text?: unknown }).text;
640
- if (typeof text === "string") parts.push(text);
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
- lastContent = answer || (aborted ? "Aborted" : "(no output)");
1039
- details.status = aborted ? "aborted" : "done";
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
 
@@ -557,6 +557,26 @@ function extractTextFromContent(content: unknown): string {
557
557
  return parts.join("\n\n").trim();
558
558
  }
559
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
+
560
580
  function parseVersionScore(text: string): number {
561
581
  const matches = text.match(/\d+(?:\.\d+){0,2}/g) ?? [];
562
582
  let best = 0;
@@ -817,6 +837,9 @@ async function runOracle(
817
837
  let finalOutput = "";
818
838
  let stderr = "";
819
839
 
840
+ let lastStopReason: string | undefined;
841
+ let lastErrorMessage: string | undefined;
842
+
820
843
  const details: OracleDetails = {
821
844
  ...selection,
822
845
  includeBash,
@@ -888,6 +911,11 @@ async function runOracle(
888
911
  if (text) finalOutput = text;
889
912
  currentText = "";
890
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
+
891
919
  const messageUsage = event.message.usage;
892
920
  if (messageUsage) {
893
921
  usage.turns += 1;
@@ -946,6 +974,19 @@ async function runOracle(
946
974
  return { ok: false, error: "Oracle was aborted.", details };
947
975
  }
948
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
+
949
990
  if (exitCode !== 0) {
950
991
  return {
951
992
  ok: false,
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@diegopetrucci/pi-oracle",
3
- "version": "0.1.12",
3
+ "version": "0.1.13",
4
4
  "description": "An Amp-style oracle extension for pi that consults the strongest reasoning model on your current provider.",
5
5
  "keywords": [
6
6
  "pi-package",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@diegopetrucci/pi-extensions",
3
- "version": "0.1.45",
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",