@diegopetrucci/pi-librarian 0.1.6 → 0.1.8
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/.pi-fleet-tested-version +1 -1
- package/index.ts +199 -85
- package/package.json +1 -1
package/.pi-fleet-tested-version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.
|
|
1
|
+
0.79.10
|
package/index.ts
CHANGED
|
@@ -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
|
}
|
package/package.json
CHANGED