@diegopetrucci/pi-extensions 0.1.52 → 0.1.53
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/extensions/contrarian/index.ts +126 -33
- package/extensions/contrarian/package.json +1 -1
- package/extensions/librarian/index.ts +199 -85
- package/extensions/librarian/package.json +1 -1
- package/extensions/oracle/index.ts +126 -33
- package/extensions/oracle/package.json +1 -1
- package/package.json +1 -1
|
@@ -619,6 +619,19 @@ function isTransientErrorMessage(message: string): boolean {
|
|
|
619
619
|
return TRANSIENT_ERROR_PATTERN.test(message);
|
|
620
620
|
}
|
|
621
621
|
|
|
622
|
+
// A model the catalog advertises but the active provider/subscription cannot
|
|
623
|
+
// serve surfaces as a not-found/404-style error (legacy snapshots or
|
|
624
|
+
// access-gated tiers). These are NOT transient: the same model keeps failing,
|
|
625
|
+
// so callers should fall back to a different model instead of retrying it.
|
|
626
|
+
const MODEL_AVAILABILITY_ERROR_PATTERN =
|
|
627
|
+
/\b(?:404|403|not[_ ]?found(?:[_ ]?error)?|model[_ ]?not[_ ]?found(?:[_ ]?error)?|no such model|unknown model|does not exist|is not available|not available|model[_ ]?not[_ ]?available|unsupported model|invalid model|forbidden|access[ _-]?denied|permission[ _-]?denied|not[ _-]?entitled|do(?:es)? not have access)\b/i;
|
|
628
|
+
|
|
629
|
+
function isModelAvailabilityError(message: string | undefined): boolean {
|
|
630
|
+
if (!message) return false;
|
|
631
|
+
if (isTransientErrorMessage(message)) return false;
|
|
632
|
+
return MODEL_AVAILABILITY_ERROR_PATTERN.test(message);
|
|
633
|
+
}
|
|
634
|
+
|
|
622
635
|
function formatContrarianModelError(stopReason: "error" | "aborted", errorMessage: string | undefined): string {
|
|
623
636
|
const trimmed = errorMessage?.trim();
|
|
624
637
|
if (stopReason === "aborted") {
|
|
@@ -810,10 +823,48 @@ async function findAvailableModel(
|
|
|
810
823
|
return undefined;
|
|
811
824
|
}
|
|
812
825
|
|
|
826
|
+
function toContrarianSelection(
|
|
827
|
+
model: PiModel,
|
|
828
|
+
thinkingLevelOverride: ThinkingLevel | undefined,
|
|
829
|
+
reason: string,
|
|
830
|
+
): ContrarianSelection {
|
|
831
|
+
const thinking = resolveThinkingLevel(model, thinkingLevelOverride);
|
|
832
|
+
return {
|
|
833
|
+
modelRef: `${model.provider}/${model.id}`,
|
|
834
|
+
provider: model.provider,
|
|
835
|
+
modelId: model.id,
|
|
836
|
+
modelName: model.name,
|
|
837
|
+
thinkingLevel: thinking.effective,
|
|
838
|
+
...(thinking.clamped ? { requestedThinkingLevel: thinking.requested, thinkingLevelClamped: true } : {}),
|
|
839
|
+
autoSelected: true,
|
|
840
|
+
selectionReason: appendThinkingLevelClampReason(reason, thinking),
|
|
841
|
+
};
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
function buildSessionFallbackSelection(
|
|
845
|
+
ctx: { model?: PiModel },
|
|
846
|
+
thinkingLevelOverride: ThinkingLevel | undefined,
|
|
847
|
+
): ContrarianSelection | undefined {
|
|
848
|
+
const model = ctx.model;
|
|
849
|
+
if (!model) return undefined;
|
|
850
|
+
return toContrarianSelection(
|
|
851
|
+
model,
|
|
852
|
+
thinkingLevelOverride,
|
|
853
|
+
`Used the current session model ${model.provider}/${model.id} as a final fallback.`,
|
|
854
|
+
);
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
function withFallbackReason(selection: ContrarianSelection, previous: ContrarianSelection): ContrarianSelection {
|
|
858
|
+
return {
|
|
859
|
+
...selection,
|
|
860
|
+
selectionReason: `${selection.selectionReason} (Fell back from ${previous.modelRef} after a model-availability error.)`,
|
|
861
|
+
};
|
|
862
|
+
}
|
|
863
|
+
|
|
813
864
|
async function selectContrarianModel(
|
|
814
865
|
ctx: { model?: PiModel; modelRegistry: { getAvailable(): PiModel[] | Promise<PiModel[]> } },
|
|
815
866
|
thinkingLevelOverride?: ThinkingLevel,
|
|
816
|
-
): Promise<{ ok: true; selection: ContrarianSelection } | { ok: false; error: string }> {
|
|
867
|
+
): Promise<{ ok: true; selection: ContrarianSelection; ordered: ContrarianSelection[] } | { ok: false; error: string }> {
|
|
817
868
|
const available = await ctx.modelRegistry.getAvailable();
|
|
818
869
|
if (available.length === 0) {
|
|
819
870
|
return {
|
|
@@ -884,29 +935,25 @@ async function selectContrarianModel(
|
|
|
884
935
|
reason = "No reasoning models were available, so the top-ranked model across all available providers was used.";
|
|
885
936
|
}
|
|
886
937
|
|
|
938
|
+
const sorted = [...candidates].sort((a, b) => rankModel(b) - rankModel(a));
|
|
887
939
|
const preferred = providerForPreferences
|
|
888
940
|
? selectPreferredModel(candidates, providerForPreferences)
|
|
889
941
|
: selectPreferredAcrossProviders(candidates);
|
|
890
|
-
const
|
|
891
|
-
|
|
892
|
-
const
|
|
893
|
-
|
|
894
|
-
|
|
942
|
+
const orderedModels = preferred ? [preferred, ...sorted.filter((model) => model !== preferred)] : sorted;
|
|
943
|
+
|
|
944
|
+
const ordered = orderedModels.map((model, index) =>
|
|
945
|
+
toContrarianSelection(
|
|
946
|
+
model,
|
|
947
|
+
thinkingLevelOverride,
|
|
948
|
+
index === 0
|
|
949
|
+
? preferred
|
|
950
|
+
? `Selected ${model.id} via the hardcoded preference lists while preferring an opposite provider/model family.`
|
|
951
|
+
: reason
|
|
952
|
+
: `Auto-selected ${model.provider}/${model.id} as a lower-ranked fallback.`,
|
|
953
|
+
),
|
|
895
954
|
);
|
|
896
955
|
|
|
897
|
-
return {
|
|
898
|
-
ok: true,
|
|
899
|
-
selection: {
|
|
900
|
-
modelRef: `${winner.provider}/${winner.id}`,
|
|
901
|
-
provider: winner.provider,
|
|
902
|
-
modelId: winner.id,
|
|
903
|
-
modelName: winner.name,
|
|
904
|
-
thinkingLevel: thinking.effective,
|
|
905
|
-
...(thinking.clamped ? { requestedThinkingLevel: thinking.requested, thinkingLevelClamped: true } : {}),
|
|
906
|
-
autoSelected: true,
|
|
907
|
-
selectionReason,
|
|
908
|
-
},
|
|
909
|
-
};
|
|
956
|
+
return { ok: true, selection: ordered[0], ordered };
|
|
910
957
|
}
|
|
911
958
|
|
|
912
959
|
function updateContrarianUi(ctx: ExtensionContext, activeRuns: Map<string, ContrarianUiRun>): void {
|
|
@@ -1314,7 +1361,16 @@ export default function contrarianExtension(pi: ExtensionAPI) {
|
|
|
1314
1361
|
updateContrarianUi(ctx, activeRuns);
|
|
1315
1362
|
|
|
1316
1363
|
try {
|
|
1317
|
-
|
|
1364
|
+
const attempts: ContrarianSelection[] = [];
|
|
1365
|
+
const seen = new Set<string>();
|
|
1366
|
+
const pushAttempt = (candidate: ContrarianSelection | undefined) => {
|
|
1367
|
+
if (!candidate) return;
|
|
1368
|
+
const key = `${candidate.provider}/${candidate.modelId}`.toLowerCase();
|
|
1369
|
+
if (seen.has(key)) return;
|
|
1370
|
+
seen.add(key);
|
|
1371
|
+
attempts.push(candidate);
|
|
1372
|
+
};
|
|
1373
|
+
|
|
1318
1374
|
if (configuredModel) {
|
|
1319
1375
|
const modelRef = configuredModel;
|
|
1320
1376
|
const matched = await findAvailableModel(ctx, modelRef);
|
|
@@ -1333,7 +1389,7 @@ export default function contrarianExtension(pi: ExtensionAPI) {
|
|
|
1333
1389
|
: usedToolOverride
|
|
1334
1390
|
? "Used the explicit model override provided in the tool call. The model was not matched against the authenticated model list, so the reasoning level fallback was applied."
|
|
1335
1391
|
: "Used the configured default contrarian model. The model was not matched against the authenticated model list, so the reasoning level fallback was applied.";
|
|
1336
|
-
|
|
1392
|
+
pushAttempt({
|
|
1337
1393
|
modelRef: matched ? `${matched.provider}/${matched.id}` : modelRef,
|
|
1338
1394
|
provider,
|
|
1339
1395
|
modelId,
|
|
@@ -1344,7 +1400,7 @@ export default function contrarianExtension(pi: ExtensionAPI) {
|
|
|
1344
1400
|
: {}),
|
|
1345
1401
|
autoSelected: false,
|
|
1346
1402
|
selectionReason,
|
|
1347
|
-
};
|
|
1403
|
+
});
|
|
1348
1404
|
} else {
|
|
1349
1405
|
const selectionResult = await selectContrarianModel(ctx, configuredThinkingLevel);
|
|
1350
1406
|
if (!selectionResult.ok) {
|
|
@@ -1367,11 +1423,14 @@ export default function contrarianExtension(pi: ExtensionAPI) {
|
|
|
1367
1423
|
},
|
|
1368
1424
|
};
|
|
1369
1425
|
}
|
|
1370
|
-
|
|
1426
|
+
for (const candidate of selectionResult.ordered) pushAttempt(candidate);
|
|
1371
1427
|
}
|
|
1372
1428
|
|
|
1373
|
-
|
|
1374
|
-
|
|
1429
|
+
// Keep the known-good session model as a final fallback: the catalog can
|
|
1430
|
+
// advertise models the active provider/subscription cannot actually serve
|
|
1431
|
+
// (legacy snapshots, access-gated tiers), which fail with a not-found/404
|
|
1432
|
+
// error. Falling back lets the run degrade gracefully instead of hard-failing.
|
|
1433
|
+
pushAttempt(buildSessionFallbackSelection(ctx, configuredThinkingLevel));
|
|
1375
1434
|
|
|
1376
1435
|
const handleUpdate = (partial: { content: Array<{ type: "text"; text: string }>; details: ContrarianDetails }) => {
|
|
1377
1436
|
uiRun.preview = partial.content[0]?.text ?? uiRun.preview;
|
|
@@ -1379,17 +1438,51 @@ export default function contrarianExtension(pi: ExtensionAPI) {
|
|
|
1379
1438
|
onUpdate?.(partial);
|
|
1380
1439
|
};
|
|
1381
1440
|
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1441
|
+
let lastErrorResult: { ok: false; error: string; details: ContrarianDetails } | undefined;
|
|
1442
|
+
for (let index = 0; index < attempts.length; index++) {
|
|
1443
|
+
const previous = index > 0 ? attempts[index - 1] : undefined;
|
|
1444
|
+
const attempt = previous ? withFallbackReason(attempts[index], previous) : attempts[index];
|
|
1445
|
+
uiRun.selection = attempt;
|
|
1446
|
+
updateContrarianUi(ctx, activeRuns);
|
|
1447
|
+
|
|
1448
|
+
const result = await runContrarian(attempt, params, signal, handleUpdate, ctx.cwd);
|
|
1449
|
+
if (result.ok) {
|
|
1450
|
+
return {
|
|
1451
|
+
content: [{ type: "text", text: result.output }],
|
|
1452
|
+
details: result.details,
|
|
1453
|
+
};
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
lastErrorResult = result;
|
|
1457
|
+
const canFallBack = index < attempts.length - 1 && isModelAvailabilityError(result.error);
|
|
1458
|
+
if (!canFallBack) {
|
|
1459
|
+
return {
|
|
1460
|
+
content: [{ type: "text", text: result.error }],
|
|
1461
|
+
details: result.details,
|
|
1462
|
+
};
|
|
1463
|
+
}
|
|
1388
1464
|
}
|
|
1389
1465
|
|
|
1390
1466
|
return {
|
|
1391
|
-
content: [
|
|
1392
|
-
|
|
1467
|
+
content: [
|
|
1468
|
+
{ type: "text", text: lastErrorResult?.error ?? "Contrarian could not select an available model." },
|
|
1469
|
+
],
|
|
1470
|
+
details:
|
|
1471
|
+
lastErrorResult?.details ?? {
|
|
1472
|
+
modelRef: "",
|
|
1473
|
+
provider: ctx.model?.provider ?? "unknown",
|
|
1474
|
+
modelId: "",
|
|
1475
|
+
modelName: undefined,
|
|
1476
|
+
thinkingLevel: configuredThinkingLevel ?? DEFAULT_THINKING_LEVEL,
|
|
1477
|
+
autoSelected: true,
|
|
1478
|
+
selectionReason: "No authenticated models were available to run the contrarian.",
|
|
1479
|
+
includeBash: params.includeBash ?? false,
|
|
1480
|
+
usage: createEmptyUsage(),
|
|
1481
|
+
stderr: "",
|
|
1482
|
+
exitCode: 1,
|
|
1483
|
+
durationMs: 0,
|
|
1484
|
+
cwd: params.cwd ?? ctx.cwd,
|
|
1485
|
+
},
|
|
1393
1486
|
};
|
|
1394
1487
|
} finally {
|
|
1395
1488
|
activeRuns.delete(toolCallId);
|
|
@@ -477,53 +477,104 @@ async function findAvailableModel(
|
|
|
477
477
|
return undefined;
|
|
478
478
|
}
|
|
479
479
|
|
|
480
|
-
|
|
480
|
+
// A model the catalog advertises but the active provider/subscription cannot
|
|
481
|
+
// serve surfaces as a not-found/404-style error (legacy snapshots or
|
|
482
|
+
// access-gated tiers). These are NOT transient: the same model keeps failing,
|
|
483
|
+
// so we fall back to the next candidate instead of retrying it.
|
|
484
|
+
const MODEL_AVAILABILITY_ERROR_PATTERN =
|
|
485
|
+
/\b(?:404|403|not[_ ]?found(?:[_ ]?error)?|model[_ ]?not[_ ]?found(?:[_ ]?error)?|no such model|unknown model|does not exist|is not available|not available|model[_ ]?not[_ ]?available|unsupported model|invalid model|forbidden|access[ _-]?denied|permission[ _-]?denied|not[ _-]?entitled|do(?:es)? not have access)\b/i;
|
|
486
|
+
|
|
487
|
+
function isModelAvailabilityError(message: string | undefined): boolean {
|
|
488
|
+
if (!message) return false;
|
|
489
|
+
return MODEL_AVAILABILITY_ERROR_PATTERN.test(message);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
type LibrarianCandidate = { model: any; details: LibrarianModelDetails };
|
|
493
|
+
|
|
494
|
+
function withLibrarianFallbackReason(candidate: LibrarianCandidate, previous: LibrarianCandidate): LibrarianCandidate {
|
|
495
|
+
return {
|
|
496
|
+
model: candidate.model,
|
|
497
|
+
details: {
|
|
498
|
+
...candidate.details,
|
|
499
|
+
selectionReason: `${candidate.details.selectionReason} (Fell back from ${previous.details.modelRef} after a model-availability error.)`,
|
|
500
|
+
},
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// Build an ordered list of model candidates to try. The catalog can advertise
|
|
505
|
+
// models the active provider/subscription cannot actually serve, so callers run
|
|
506
|
+
// these in order and skip ones that fail with a model-availability error,
|
|
507
|
+
// ultimately falling back to the known-good current session model.
|
|
508
|
+
async function buildLibrarianCandidates(
|
|
481
509
|
ctx: { model?: any; modelRegistry: { getAvailable(): any[] | Promise<any[]> } },
|
|
482
510
|
modelPreference: string | undefined,
|
|
483
511
|
thinkingLevel: ThinkingLevel,
|
|
484
|
-
): Promise<
|
|
512
|
+
): Promise<LibrarianCandidate[]> {
|
|
513
|
+
const candidates: LibrarianCandidate[] = [];
|
|
514
|
+
const seen = new Set<string>();
|
|
515
|
+
const push = (model: any, details: LibrarianModelDetails) => {
|
|
516
|
+
if (!model) return;
|
|
517
|
+
const key = `${model.provider}/${model.id}`.toLowerCase();
|
|
518
|
+
if (seen.has(key)) return;
|
|
519
|
+
seen.add(key);
|
|
520
|
+
candidates.push({ model, details });
|
|
521
|
+
};
|
|
522
|
+
|
|
485
523
|
const normalized = modelPreference?.trim();
|
|
524
|
+
let configuredMatched: any | undefined;
|
|
486
525
|
if (normalized) {
|
|
487
|
-
|
|
488
|
-
if (
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
selectionReason: "Using the configured librarian model.",
|
|
498
|
-
},
|
|
499
|
-
};
|
|
526
|
+
configuredMatched = await findAvailableModel(ctx, normalized);
|
|
527
|
+
if (configuredMatched) {
|
|
528
|
+
push(configuredMatched, {
|
|
529
|
+
modelRef: modelRef(configuredMatched),
|
|
530
|
+
provider: configuredMatched.provider,
|
|
531
|
+
modelId: configuredMatched.id,
|
|
532
|
+
thinkingLevel,
|
|
533
|
+
autoSelected: false,
|
|
534
|
+
selectionReason: "Using the configured librarian model.",
|
|
535
|
+
});
|
|
500
536
|
}
|
|
501
537
|
}
|
|
502
538
|
|
|
503
539
|
const available = await ctx.modelRegistry.getAvailable();
|
|
504
540
|
const preferred = available.filter(isPreferredFastLibrarianModel);
|
|
505
|
-
const
|
|
506
|
-
? [...preferred].sort((a, b) => rankPreferredFastLibrarianModel(a) - rankPreferredFastLibrarianModel(b))
|
|
507
|
-
: [...available].sort((a, b) => rankLibrarianModel(a) - rankLibrarianModel(b))
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
541
|
+
const ranked = preferred.length > 0
|
|
542
|
+
? [...preferred].sort((a, b) => rankPreferredFastLibrarianModel(a) - rankPreferredFastLibrarianModel(b))
|
|
543
|
+
: [...available].sort((a, b) => rankLibrarianModel(a) - rankLibrarianModel(b));
|
|
544
|
+
const fallbackText = normalized && !configuredMatched
|
|
545
|
+
? ` Configured model ${normalized} was unavailable, so Librarian fell back to auto-selection.`
|
|
546
|
+
: "";
|
|
547
|
+
ranked.forEach((model, index) => {
|
|
548
|
+
const topReason = preferred.length > 0
|
|
549
|
+
? `Selected a preferred fast Librarian model.${fallbackText}`
|
|
550
|
+
: `Selected the cheapest available model because no preferred fast Librarian model was available.${fallbackText}`;
|
|
551
|
+
push(model, {
|
|
552
|
+
modelRef: modelRef(model),
|
|
553
|
+
provider: model.provider,
|
|
554
|
+
modelId: model.id,
|
|
555
|
+
thinkingLevel,
|
|
556
|
+
autoSelected: true,
|
|
557
|
+
selectionReason: index === 0 ? topReason : `Auto-selected ${modelRef(model)} as a lower-ranked fallback.`,
|
|
558
|
+
});
|
|
559
|
+
});
|
|
511
560
|
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
details: {
|
|
519
|
-
modelRef: modelRef(winner),
|
|
520
|
-
provider: winner.provider,
|
|
521
|
-
modelId: winner.id,
|
|
561
|
+
// Final fallback: the active session model is known to be servable.
|
|
562
|
+
if (ctx.model) {
|
|
563
|
+
push(ctx.model, {
|
|
564
|
+
modelRef: modelRef(ctx.model),
|
|
565
|
+
provider: ctx.model.provider,
|
|
566
|
+
modelId: ctx.model.id,
|
|
522
567
|
thinkingLevel,
|
|
523
568
|
autoSelected: true,
|
|
524
|
-
selectionReason
|
|
525
|
-
}
|
|
526
|
-
}
|
|
569
|
+
selectionReason: `Used the current session model ${modelRef(ctx.model)} as a final fallback.`,
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
if (candidates.length === 0) {
|
|
574
|
+
throw new Error("No authenticated models are available for Librarian. Log in or configure an API key first.");
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
return candidates;
|
|
527
578
|
}
|
|
528
579
|
|
|
529
580
|
function resolveToolPath(cwd: string, rawPath: string): string {
|
|
@@ -944,7 +995,8 @@ export default function librarianExtension(pi: ExtensionAPI) {
|
|
|
944
995
|
parseThinkingLevel((params as { thinkingLevel?: unknown }).thinkingLevel) ??
|
|
945
996
|
explicitModel.thinkingLevel ??
|
|
946
997
|
thinkingPreference;
|
|
947
|
-
const
|
|
998
|
+
const candidates = await buildLibrarianCandidates(ctx, explicitModel.model ?? modelPreference, thinkingLevel);
|
|
999
|
+
const selectedModel = candidates[0];
|
|
948
1000
|
|
|
949
1001
|
const workspaceBase = path.join(os.tmpdir(), "pi-librarian");
|
|
950
1002
|
await fs.mkdir(workspaceBase, { recursive: true });
|
|
@@ -1045,62 +1097,124 @@ export default function librarianExtension(pi: ExtensionAPI) {
|
|
|
1045
1097
|
|
|
1046
1098
|
await resourceLoader.reload();
|
|
1047
1099
|
|
|
1048
|
-
const
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1100
|
+
const runAttempt = async (
|
|
1101
|
+
candidate: LibrarianCandidate,
|
|
1102
|
+
): Promise<{ answer: string; lastAssistant: AssistantLikeMessage | undefined }> => {
|
|
1103
|
+
details.model = candidate.details;
|
|
1104
|
+
details.turns = 0;
|
|
1105
|
+
details.toolCalls = [];
|
|
1106
|
+
emit(true);
|
|
1107
|
+
|
|
1108
|
+
const created = await createAgentSession({
|
|
1109
|
+
cwd: workspace,
|
|
1110
|
+
modelRegistry: ctx.modelRegistry,
|
|
1111
|
+
resourceLoader,
|
|
1112
|
+
sessionManager: SessionManager.inMemory(workspace),
|
|
1113
|
+
model: candidate.model,
|
|
1114
|
+
thinkingLevel,
|
|
1115
|
+
tools: ["read", "bash"],
|
|
1116
|
+
});
|
|
1057
1117
|
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1118
|
+
session = created.session as typeof session;
|
|
1119
|
+
unsubscribe = (created.session as any).subscribe((event: any) => {
|
|
1120
|
+
switch (event.type) {
|
|
1121
|
+
case "turn_end":
|
|
1122
|
+
details.turns += 1;
|
|
1123
|
+
emit();
|
|
1124
|
+
break;
|
|
1125
|
+
case "tool_execution_start":
|
|
1126
|
+
details.toolCalls.push({
|
|
1127
|
+
id: event.toolCallId,
|
|
1128
|
+
name: event.toolName,
|
|
1129
|
+
args: event.args,
|
|
1130
|
+
startedAt: Date.now(),
|
|
1131
|
+
});
|
|
1132
|
+
if (details.toolCalls.length > MAX_TOOL_CALLS_TO_KEEP) {
|
|
1133
|
+
details.toolCalls.splice(0, details.toolCalls.length - MAX_TOOL_CALLS_TO_KEEP);
|
|
1134
|
+
}
|
|
1135
|
+
emit(true);
|
|
1136
|
+
break;
|
|
1137
|
+
case "tool_execution_end": {
|
|
1138
|
+
const call = details.toolCalls.find((item) => item.id === event.toolCallId);
|
|
1139
|
+
if (call) {
|
|
1140
|
+
call.endedAt = Date.now();
|
|
1141
|
+
call.isError = event.isError;
|
|
1142
|
+
}
|
|
1143
|
+
emit(true);
|
|
1144
|
+
break;
|
|
1074
1145
|
}
|
|
1075
|
-
emit(true);
|
|
1076
|
-
break;
|
|
1077
|
-
case "tool_execution_end": {
|
|
1078
|
-
const call = details.toolCalls.find((item) => item.id === event.toolCallId);
|
|
1079
|
-
if (call) {
|
|
1080
|
-
call.endedAt = Date.now();
|
|
1081
|
-
call.isError = event.isError;
|
|
1082
|
-
}
|
|
1083
|
-
emit(true);
|
|
1084
|
-
break;
|
|
1085
1146
|
}
|
|
1147
|
+
});
|
|
1148
|
+
|
|
1149
|
+
try {
|
|
1150
|
+
if (!aborted) {
|
|
1151
|
+
const promptPromise = created.session.prompt(buildUserPrompt(query, repos, owners, maxSearchResults, details.cache), {
|
|
1152
|
+
expandPromptTemplates: false,
|
|
1153
|
+
});
|
|
1154
|
+
const timeoutPromise = new Promise<never>((_resolve, reject) => {
|
|
1155
|
+
runTimeout = setTimeout(() => {
|
|
1156
|
+
abort();
|
|
1157
|
+
reject(new Error(`Librarian timed out after ${Math.round(MAX_RUN_MS / 1000)} seconds.`));
|
|
1158
|
+
}, MAX_RUN_MS);
|
|
1159
|
+
});
|
|
1160
|
+
await Promise.race([promptPromise, timeoutPromise]);
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
const lastAssistant = session ? getLastAssistantMessage(session.state.messages) : undefined;
|
|
1164
|
+
const answer = session ? extractLastAssistantText(session.state.messages) : "";
|
|
1165
|
+
return { answer, lastAssistant };
|
|
1166
|
+
} finally {
|
|
1167
|
+
if (runTimeout) {
|
|
1168
|
+
clearTimeout(runTimeout);
|
|
1169
|
+
runTimeout = undefined;
|
|
1170
|
+
}
|
|
1171
|
+
unsubscribe?.();
|
|
1172
|
+
unsubscribe = undefined;
|
|
1173
|
+
session?.dispose();
|
|
1174
|
+
session = undefined;
|
|
1086
1175
|
}
|
|
1087
|
-
}
|
|
1176
|
+
};
|
|
1088
1177
|
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1178
|
+
let answer = "";
|
|
1179
|
+
let lastAssistant: AssistantLikeMessage | undefined;
|
|
1180
|
+
let attemptErrorReason: string | undefined;
|
|
1181
|
+
for (let index = 0; index < candidates.length; index++) {
|
|
1182
|
+
if (aborted) break;
|
|
1183
|
+
const candidate =
|
|
1184
|
+
index === 0 ? candidates[index] : withLibrarianFallbackReason(candidates[index], candidates[index - 1]);
|
|
1185
|
+
|
|
1186
|
+
let attempt: { answer: string; lastAssistant: AssistantLikeMessage | undefined };
|
|
1187
|
+
try {
|
|
1188
|
+
attempt = await runAttempt(candidate);
|
|
1189
|
+
} catch (error) {
|
|
1190
|
+
// Let aborts/timeouts propagate to the outer handler.
|
|
1191
|
+
if (aborted || isAbortLikeError(error)) throw error;
|
|
1192
|
+
// Some providers throw (rather than carry the error on the assistant
|
|
1193
|
+
// message) when a model is unavailable; treat that like a
|
|
1194
|
+
// message-carried availability error and fall back to the next model.
|
|
1195
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1196
|
+
if (index < candidates.length - 1 && isModelAvailabilityError(message)) {
|
|
1197
|
+
answer = "";
|
|
1198
|
+
lastAssistant = undefined;
|
|
1199
|
+
attemptErrorReason = `the internal subagent stopped with an error: ${message}`;
|
|
1200
|
+
continue;
|
|
1201
|
+
}
|
|
1202
|
+
throw error;
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
answer = attempt.answer;
|
|
1206
|
+
lastAssistant = attempt.lastAssistant;
|
|
1207
|
+
if (answer || aborted) break;
|
|
1208
|
+
|
|
1209
|
+
attemptErrorReason = describeNoAnswerReason(lastAssistant, details.turns);
|
|
1210
|
+
const errorMessage =
|
|
1211
|
+
lastAssistant && lastAssistant.stopReason === "error" && typeof lastAssistant.errorMessage === "string"
|
|
1212
|
+
? lastAssistant.errorMessage
|
|
1213
|
+
: undefined;
|
|
1214
|
+
const canFallBack = index < candidates.length - 1 && isModelAvailabilityError(errorMessage);
|
|
1215
|
+
if (!canFallBack) break;
|
|
1100
1216
|
}
|
|
1101
1217
|
|
|
1102
|
-
const lastAssistant = session ? getLastAssistantMessage(session.state.messages) : undefined;
|
|
1103
|
-
const answer = session ? extractLastAssistantText(session.state.messages) : "";
|
|
1104
1218
|
if (answer) {
|
|
1105
1219
|
lastContent = answer;
|
|
1106
1220
|
details.status = "done";
|
|
@@ -1109,7 +1223,7 @@ export default function librarianExtension(pi: ExtensionAPI) {
|
|
|
1109
1223
|
details.status = "aborted";
|
|
1110
1224
|
} else {
|
|
1111
1225
|
details.status = "error";
|
|
1112
|
-
const reason = describeNoAnswerReason(lastAssistant, details.turns);
|
|
1226
|
+
const reason = attemptErrorReason ?? describeNoAnswerReason(lastAssistant, details.turns);
|
|
1113
1227
|
lastContent = formatInternalFailure(details, reason);
|
|
1114
1228
|
details.error = reason;
|
|
1115
1229
|
}
|
|
@@ -612,6 +612,19 @@ function isTransientErrorMessage(message: string): boolean {
|
|
|
612
612
|
return TRANSIENT_ERROR_PATTERN.test(message);
|
|
613
613
|
}
|
|
614
614
|
|
|
615
|
+
// A model the catalog advertises but the active provider/subscription cannot
|
|
616
|
+
// serve surfaces as a not-found/404-style error (legacy snapshots or
|
|
617
|
+
// access-gated tiers). These are NOT transient: the same model keeps failing,
|
|
618
|
+
// so callers should fall back to a different model instead of retrying it.
|
|
619
|
+
const MODEL_AVAILABILITY_ERROR_PATTERN =
|
|
620
|
+
/\b(?:404|403|not[_ ]?found(?:[_ ]?error)?|model[_ ]?not[_ ]?found(?:[_ ]?error)?|no such model|unknown model|does not exist|is not available|not available|model[_ ]?not[_ ]?available|unsupported model|invalid model|forbidden|access[ _-]?denied|permission[ _-]?denied|not[ _-]?entitled|do(?:es)? not have access)\b/i;
|
|
621
|
+
|
|
622
|
+
function isModelAvailabilityError(message: string | undefined): boolean {
|
|
623
|
+
if (!message) return false;
|
|
624
|
+
if (isTransientErrorMessage(message)) return false;
|
|
625
|
+
return MODEL_AVAILABILITY_ERROR_PATTERN.test(message);
|
|
626
|
+
}
|
|
627
|
+
|
|
615
628
|
function formatOracleModelError(stopReason: "error" | "aborted", errorMessage: string | undefined): string {
|
|
616
629
|
const trimmed = errorMessage?.trim();
|
|
617
630
|
if (stopReason === "aborted") {
|
|
@@ -776,10 +789,48 @@ async function findAvailableModel(
|
|
|
776
789
|
return undefined;
|
|
777
790
|
}
|
|
778
791
|
|
|
792
|
+
function toOracleSelection(
|
|
793
|
+
model: PiModel,
|
|
794
|
+
thinkingLevelOverride: ThinkingLevel | undefined,
|
|
795
|
+
reason: string,
|
|
796
|
+
): OracleSelection {
|
|
797
|
+
const thinking = resolveThinkingLevel(model, thinkingLevelOverride);
|
|
798
|
+
return {
|
|
799
|
+
modelRef: `${model.provider}/${model.id}`,
|
|
800
|
+
provider: model.provider,
|
|
801
|
+
modelId: model.id,
|
|
802
|
+
modelName: model.name,
|
|
803
|
+
thinkingLevel: thinking.effective,
|
|
804
|
+
...(thinking.clamped ? { requestedThinkingLevel: thinking.requested, thinkingLevelClamped: true } : {}),
|
|
805
|
+
autoSelected: true,
|
|
806
|
+
selectionReason: appendThinkingLevelClampReason(reason, thinking),
|
|
807
|
+
};
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
function buildSessionFallbackSelection(
|
|
811
|
+
ctx: { model?: PiModel },
|
|
812
|
+
thinkingLevelOverride: ThinkingLevel | undefined,
|
|
813
|
+
): OracleSelection | undefined {
|
|
814
|
+
const model = ctx.model;
|
|
815
|
+
if (!model) return undefined;
|
|
816
|
+
return toOracleSelection(
|
|
817
|
+
model,
|
|
818
|
+
thinkingLevelOverride,
|
|
819
|
+
`Used the current session model ${model.provider}/${model.id} as a final fallback.`,
|
|
820
|
+
);
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
function withFallbackReason(selection: OracleSelection, previous: OracleSelection): OracleSelection {
|
|
824
|
+
return {
|
|
825
|
+
...selection,
|
|
826
|
+
selectionReason: `${selection.selectionReason} (Fell back from ${previous.modelRef} after a model-availability error.)`,
|
|
827
|
+
};
|
|
828
|
+
}
|
|
829
|
+
|
|
779
830
|
async function selectOracleModel(
|
|
780
831
|
ctx: { model?: PiModel; modelRegistry: { getAvailable(): PiModel[] | Promise<PiModel[]> } },
|
|
781
832
|
thinkingLevelOverride?: ThinkingLevel,
|
|
782
|
-
): Promise<{ ok: true; selection: OracleSelection } | { ok: false; error: string }> {
|
|
833
|
+
): Promise<{ ok: true; selection: OracleSelection; ordered: OracleSelection[] } | { ok: false; error: string }> {
|
|
783
834
|
const available = await ctx.modelRegistry.getAvailable();
|
|
784
835
|
if (available.length === 0) {
|
|
785
836
|
return {
|
|
@@ -810,27 +861,23 @@ async function selectOracleModel(
|
|
|
810
861
|
reason = "No reasoning models were available, so the top-ranked model across all available providers was used.";
|
|
811
862
|
}
|
|
812
863
|
|
|
864
|
+
const sorted = [...candidates].sort((a, b) => rankModel(b) - rankModel(a));
|
|
813
865
|
const preferred = selectPreferredModel(candidates, providerForPreferences);
|
|
814
|
-
const
|
|
815
|
-
|
|
816
|
-
const
|
|
817
|
-
|
|
818
|
-
|
|
866
|
+
const orderedModels = preferred ? [preferred, ...sorted.filter((model) => model !== preferred)] : sorted;
|
|
867
|
+
|
|
868
|
+
const ordered = orderedModels.map((model, index) =>
|
|
869
|
+
toOracleSelection(
|
|
870
|
+
model,
|
|
871
|
+
thinkingLevelOverride,
|
|
872
|
+
index === 0
|
|
873
|
+
? preferred
|
|
874
|
+
? `Selected ${model.id} via the hardcoded preference list for ${model.provider}.`
|
|
875
|
+
: reason
|
|
876
|
+
: `Auto-selected ${model.provider}/${model.id} as a lower-ranked fallback.`,
|
|
877
|
+
),
|
|
819
878
|
);
|
|
820
879
|
|
|
821
|
-
return {
|
|
822
|
-
ok: true,
|
|
823
|
-
selection: {
|
|
824
|
-
modelRef: `${winner.provider}/${winner.id}`,
|
|
825
|
-
provider: winner.provider,
|
|
826
|
-
modelId: winner.id,
|
|
827
|
-
modelName: winner.name,
|
|
828
|
-
thinkingLevel: thinking.effective,
|
|
829
|
-
...(thinking.clamped ? { requestedThinkingLevel: thinking.requested, thinkingLevelClamped: true } : {}),
|
|
830
|
-
autoSelected: true,
|
|
831
|
-
selectionReason,
|
|
832
|
-
},
|
|
833
|
-
};
|
|
880
|
+
return { ok: true, selection: ordered[0], ordered };
|
|
834
881
|
}
|
|
835
882
|
|
|
836
883
|
function updateOracleUi(ctx: ExtensionContext, activeRuns: Map<string, OracleUiRun>): void {
|
|
@@ -1237,7 +1284,16 @@ export default function oracleExtension(pi: ExtensionAPI) {
|
|
|
1237
1284
|
updateOracleUi(ctx, activeRuns);
|
|
1238
1285
|
|
|
1239
1286
|
try {
|
|
1240
|
-
|
|
1287
|
+
const attempts: OracleSelection[] = [];
|
|
1288
|
+
const seen = new Set<string>();
|
|
1289
|
+
const pushAttempt = (candidate: OracleSelection | undefined) => {
|
|
1290
|
+
if (!candidate) return;
|
|
1291
|
+
const key = `${candidate.provider}/${candidate.modelId}`.toLowerCase();
|
|
1292
|
+
if (seen.has(key)) return;
|
|
1293
|
+
seen.add(key);
|
|
1294
|
+
attempts.push(candidate);
|
|
1295
|
+
};
|
|
1296
|
+
|
|
1241
1297
|
if (configuredModel) {
|
|
1242
1298
|
const modelRef = configuredModel;
|
|
1243
1299
|
const matched = await findAvailableModel(ctx, modelRef);
|
|
@@ -1256,7 +1312,7 @@ export default function oracleExtension(pi: ExtensionAPI) {
|
|
|
1256
1312
|
: usedToolOverride
|
|
1257
1313
|
? "Used the explicit model override provided in the tool call. The model was not matched against the authenticated model list, so the reasoning level fallback was applied."
|
|
1258
1314
|
: "Used the configured default oracle model. The model was not matched against the authenticated model list, so the reasoning level fallback was applied.";
|
|
1259
|
-
|
|
1315
|
+
pushAttempt({
|
|
1260
1316
|
modelRef: matched ? `${matched.provider}/${matched.id}` : modelRef,
|
|
1261
1317
|
provider,
|
|
1262
1318
|
modelId,
|
|
@@ -1267,7 +1323,7 @@ export default function oracleExtension(pi: ExtensionAPI) {
|
|
|
1267
1323
|
: {}),
|
|
1268
1324
|
autoSelected: false,
|
|
1269
1325
|
selectionReason,
|
|
1270
|
-
};
|
|
1326
|
+
});
|
|
1271
1327
|
} else {
|
|
1272
1328
|
const selectionResult = await selectOracleModel(ctx, configuredThinkingLevel);
|
|
1273
1329
|
if (!selectionResult.ok) {
|
|
@@ -1290,11 +1346,14 @@ export default function oracleExtension(pi: ExtensionAPI) {
|
|
|
1290
1346
|
},
|
|
1291
1347
|
};
|
|
1292
1348
|
}
|
|
1293
|
-
|
|
1349
|
+
for (const candidate of selectionResult.ordered) pushAttempt(candidate);
|
|
1294
1350
|
}
|
|
1295
1351
|
|
|
1296
|
-
|
|
1297
|
-
|
|
1352
|
+
// Keep the known-good session model as a final fallback: the catalog can
|
|
1353
|
+
// advertise models the active provider/subscription cannot actually serve
|
|
1354
|
+
// (legacy snapshots, access-gated tiers), which fail with a not-found/404
|
|
1355
|
+
// error. Falling back lets the run degrade gracefully instead of hard-failing.
|
|
1356
|
+
pushAttempt(buildSessionFallbackSelection(ctx, configuredThinkingLevel));
|
|
1298
1357
|
|
|
1299
1358
|
const handleUpdate = (partial: { content: Array<{ type: "text"; text: string }>; details: OracleDetails }) => {
|
|
1300
1359
|
uiRun.preview = partial.content[0]?.text ?? uiRun.preview;
|
|
@@ -1302,17 +1361,51 @@ export default function oracleExtension(pi: ExtensionAPI) {
|
|
|
1302
1361
|
onUpdate?.(partial);
|
|
1303
1362
|
};
|
|
1304
1363
|
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1364
|
+
let lastErrorResult: { ok: false; error: string; details: OracleDetails } | undefined;
|
|
1365
|
+
for (let index = 0; index < attempts.length; index++) {
|
|
1366
|
+
const previous = index > 0 ? attempts[index - 1] : undefined;
|
|
1367
|
+
const attempt = previous ? withFallbackReason(attempts[index], previous) : attempts[index];
|
|
1368
|
+
uiRun.selection = attempt;
|
|
1369
|
+
updateOracleUi(ctx, activeRuns);
|
|
1370
|
+
|
|
1371
|
+
const result = await runOracle(attempt, params, signal, handleUpdate, ctx.cwd);
|
|
1372
|
+
if (result.ok) {
|
|
1373
|
+
return {
|
|
1374
|
+
content: [{ type: "text", text: result.output }],
|
|
1375
|
+
details: result.details,
|
|
1376
|
+
};
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
lastErrorResult = result;
|
|
1380
|
+
const canFallBack = index < attempts.length - 1 && isModelAvailabilityError(result.error);
|
|
1381
|
+
if (!canFallBack) {
|
|
1382
|
+
return {
|
|
1383
|
+
content: [{ type: "text", text: result.error }],
|
|
1384
|
+
details: result.details,
|
|
1385
|
+
};
|
|
1386
|
+
}
|
|
1311
1387
|
}
|
|
1312
1388
|
|
|
1313
1389
|
return {
|
|
1314
|
-
content: [
|
|
1315
|
-
|
|
1390
|
+
content: [
|
|
1391
|
+
{ type: "text", text: lastErrorResult?.error ?? "Oracle could not select an available model." },
|
|
1392
|
+
],
|
|
1393
|
+
details:
|
|
1394
|
+
lastErrorResult?.details ?? {
|
|
1395
|
+
modelRef: "",
|
|
1396
|
+
provider: ctx.model?.provider ?? "unknown",
|
|
1397
|
+
modelId: "",
|
|
1398
|
+
modelName: undefined,
|
|
1399
|
+
thinkingLevel: configuredThinkingLevel ?? DEFAULT_THINKING_LEVEL,
|
|
1400
|
+
autoSelected: true,
|
|
1401
|
+
selectionReason: "No authenticated models were available to run the oracle.",
|
|
1402
|
+
includeBash: params.includeBash ?? false,
|
|
1403
|
+
usage: createEmptyUsage(),
|
|
1404
|
+
stderr: "",
|
|
1405
|
+
exitCode: 1,
|
|
1406
|
+
durationMs: 0,
|
|
1407
|
+
cwd: params.cwd ?? ctx.cwd,
|
|
1408
|
+
},
|
|
1316
1409
|
};
|
|
1317
1410
|
} finally {
|
|
1318
1411
|
activeRuns.delete(toolCallId);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@diegopetrucci/pi-extensions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.53",
|
|
4
4
|
"description": "A collection of pi extensions and skills for annotation UIs, context management, workflow audits, contrarian review, 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",
|