@f5-sales-demo/xcsh 22.2.0 → 22.3.1
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/CHANGELOG.md +7 -0
- package/package.json +8 -8
- package/src/cli/plugin-cli.ts +17 -7
- package/src/cli/update-cli.ts +85 -21
- package/src/cli/update-recommendation.ts +31 -11
- package/src/internal-urls/api-catalog-qmd-index.generated.ts +2 -2
- package/src/internal-urls/build-info.generated.ts +8 -8
- package/src/internal-urls/docs-index.generated.ts +2 -2
- package/src/internal-urls/documentation-repository.ts +5 -1
- package/src/internal-urls/documentation-resolve.ts +1 -1
- package/src/internal-urls/documentation-snapshot.ts +28 -1
- package/src/internal-urls/knowledge-classifier.ts +19 -5
- package/src/modes/components/model-selector.ts +148 -40
- package/src/modes/controllers/model-selection.ts +35 -4
- package/src/prompts/rules/llms-search.md +3 -1
- package/src/prompts/system/system-prompt.md +2 -0
- package/src/qmd-smoke.ts +58 -15
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { ThinkingLevel } from "@f5-sales-demo/pi-agent-core";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
type CodexContextTier,
|
|
4
|
+
getSupportedReasoningEfforts,
|
|
5
|
+
type Model,
|
|
6
|
+
modelsAreEqual,
|
|
7
|
+
ReasoningEffort,
|
|
8
|
+
} from "@f5-sales-demo/pi-ai";
|
|
3
9
|
import {
|
|
4
10
|
Container,
|
|
5
11
|
getKeybindings,
|
|
@@ -58,6 +64,10 @@ export function reasoningLabel(level: ThinkingLevel): string {
|
|
|
58
64
|
}[level] ?? level
|
|
59
65
|
);
|
|
60
66
|
}
|
|
67
|
+
|
|
68
|
+
function formatContextSize(tokens: number): string {
|
|
69
|
+
return `${Math.round(tokens / 1_000)}K`;
|
|
70
|
+
}
|
|
61
71
|
export interface ModelNavigationContext {
|
|
62
72
|
initialSearchInput?: string;
|
|
63
73
|
initialProvider?: string;
|
|
@@ -312,6 +322,8 @@ export interface ModelSelection {
|
|
|
312
322
|
model: Model;
|
|
313
323
|
selector: string;
|
|
314
324
|
thinkingLevel: ThinkingLevel;
|
|
325
|
+
/** Provider-wide context policy, present only for supported OpenAI routes. */
|
|
326
|
+
contextTier?: CodexContextTier;
|
|
315
327
|
role?: string;
|
|
316
328
|
}
|
|
317
329
|
type RoleSelectCallback = (selection: ModelSelection) => boolean | undefined | Promise<boolean | undefined>;
|
|
@@ -398,19 +410,28 @@ export class ModelSelectorComponent extends Container {
|
|
|
398
410
|
];
|
|
399
411
|
if (this.#isMenuOpen && selected) {
|
|
400
412
|
details.push(...wrapTextWithAnsi(selected.selector, inner));
|
|
401
|
-
title =
|
|
413
|
+
title =
|
|
414
|
+
this.#menuStep === "thinking"
|
|
415
|
+
? "Reasoning"
|
|
416
|
+
: this.#menuStep === "context"
|
|
417
|
+
? "Context window"
|
|
418
|
+
: getModelDisplayName(selected.model);
|
|
402
419
|
const labels =
|
|
403
|
-
this.#menuStep === "
|
|
404
|
-
? this.#
|
|
405
|
-
: this.#menuStep === "
|
|
406
|
-
? this.#
|
|
407
|
-
: this.#
|
|
420
|
+
this.#menuStep === "context"
|
|
421
|
+
? this.#contextActions(selected.model).map(action => action.label)
|
|
422
|
+
: this.#menuStep === "scope"
|
|
423
|
+
? this.#scopeActions()
|
|
424
|
+
: this.#menuStep === "role"
|
|
425
|
+
? this.#menuRoleActions.map(action => action.label)
|
|
426
|
+
: this.#getThinkingLevelsForModel(selected.model).map(reasoningLabel);
|
|
408
427
|
purpose =
|
|
409
428
|
this.#menuStep === "thinking"
|
|
410
|
-
?
|
|
411
|
-
: this.#menuStep === "
|
|
412
|
-
? "
|
|
413
|
-
:
|
|
429
|
+
? "Choose reasoning for this model."
|
|
430
|
+
: this.#menuStep === "context"
|
|
431
|
+
? "This provider-wide setting controls the usable context window. Scope controls only the model and reasoning assignment."
|
|
432
|
+
: this.#menuStep === "scope"
|
|
433
|
+
? "Choose where this model applies."
|
|
434
|
+
: "Choose a specialist role.";
|
|
414
435
|
body = labels.map((label, i) => selectorRow([label], [inner - 2], i === this.#menuSelectedIndex));
|
|
415
436
|
selectedBodyIndex = this.#menuSelectedIndex;
|
|
416
437
|
if (this.#menuStep === "scope")
|
|
@@ -422,10 +443,17 @@ export class ModelSelectorComponent extends Container {
|
|
|
422
443
|
][this.#menuSelectedIndex],
|
|
423
444
|
);
|
|
424
445
|
else if (this.#menuStep === "thinking") details.push(`Reasoning: ${labels[this.#menuSelectedIndex]}`);
|
|
446
|
+
else if (this.#menuStep === "context") {
|
|
447
|
+
const context = this.#contextActions(selected.model)[this.#menuSelectedIndex];
|
|
448
|
+
if (context) {
|
|
449
|
+
details.push(`Usable window: ${formatContextSize(context.effectiveContextWindow)}.`);
|
|
450
|
+
details.push(`Automatic compaction begins at ${formatContextSize(context.autoCompactTokenLimit)}.`);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
425
453
|
if (this.#errorMessage)
|
|
426
454
|
details.push(`Could not apply: ${String(this.#errorMessage)}. ${selectorKeys("confirm")} to retry.`);
|
|
427
455
|
footer = [
|
|
428
|
-
selectorNavigationHint(this.#menuStep === "
|
|
456
|
+
selectorNavigationHint(this.#menuStep === "scope" || this.#menuStep === "role" ? "continue" : "confirm"),
|
|
429
457
|
selectorCancelHint(),
|
|
430
458
|
];
|
|
431
459
|
} else {
|
|
@@ -585,13 +613,15 @@ export class ModelSelectorComponent extends Container {
|
|
|
585
613
|
// Context menu state
|
|
586
614
|
#isMenuOpen: boolean = false;
|
|
587
615
|
#menuSelectedIndex: number = 0;
|
|
588
|
-
#menuStep: "
|
|
616
|
+
#menuStep: "thinking" | "context" | "scope" | "role" = "thinking";
|
|
589
617
|
#menuScope: ModelSelection["scope"] = "conversation";
|
|
590
618
|
#menuItem?: ModelItem | CanonicalModelItem;
|
|
591
619
|
#applying = false;
|
|
592
620
|
#onLogin?: () => void;
|
|
593
621
|
#currentThinkingLevel: ThinkingLevel = ThinkingLevel.Inherit;
|
|
594
622
|
#menuSelectedRole: string | null = null;
|
|
623
|
+
#menuThinkingLevel: ThinkingLevel = ThinkingLevel.Inherit;
|
|
624
|
+
#menuContextTier?: CodexContextTier;
|
|
595
625
|
|
|
596
626
|
constructor(
|
|
597
627
|
tui: TUI,
|
|
@@ -1197,6 +1227,47 @@ export class ModelSelectorComponent extends Container {
|
|
|
1197
1227
|
return foundIndex >= 0 ? foundIndex : 0;
|
|
1198
1228
|
}
|
|
1199
1229
|
|
|
1230
|
+
#contextSettingFor(model: Model): "providers.openaiContextTier" | "providers.litellmContextTier" | undefined {
|
|
1231
|
+
if (model.provider === "openai-codex") return "providers.openaiContextTier";
|
|
1232
|
+
if (model.provider === "litellm") return "providers.litellmContextTier";
|
|
1233
|
+
return undefined;
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
#contextActions(model: Model): Array<{
|
|
1237
|
+
tier: CodexContextTier;
|
|
1238
|
+
label: string;
|
|
1239
|
+
effectiveContextWindow: number;
|
|
1240
|
+
autoCompactTokenLimit: number;
|
|
1241
|
+
}> {
|
|
1242
|
+
const setting = this.#contextSettingFor(model);
|
|
1243
|
+
if (!setting || !model.maxContextWindow) return [];
|
|
1244
|
+
// The registry has already clamped this to the safe advertised provider limit.
|
|
1245
|
+
const providerMaximum = model.providerContextWindow ?? model.maxContextWindow;
|
|
1246
|
+
const standard = Math.min(272_000, providerMaximum);
|
|
1247
|
+
const codexMaximum = Math.min(model.maxContextWindow, providerMaximum);
|
|
1248
|
+
const effectivePercent = model.effectiveContextWindowPercent ?? 95;
|
|
1249
|
+
const compactPercent = model.autoCompactThresholdPercent ?? 90;
|
|
1250
|
+
const current = this.#settings.get(setting) as CodexContextTier;
|
|
1251
|
+
return [
|
|
1252
|
+
["standard", "Standard", standard],
|
|
1253
|
+
["codex-max", "Codex maximum", codexMaximum],
|
|
1254
|
+
["provider-max", "Provider maximum", providerMaximum],
|
|
1255
|
+
].map(([tier, label, size]) => ({
|
|
1256
|
+
tier: tier as CodexContextTier,
|
|
1257
|
+
label: `${label} — ${formatContextSize(size as number)}${tier === current ? " (current/default)" : ""}`,
|
|
1258
|
+
effectiveContextWindow: Math.floor(((size as number) * effectivePercent) / 100),
|
|
1259
|
+
autoCompactTokenLimit: Math.floor(((size as number) * compactPercent) / 100),
|
|
1260
|
+
}));
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
#contextPreselectIndex(model: Model): number {
|
|
1264
|
+
const setting = this.#contextSettingFor(model);
|
|
1265
|
+
if (!setting) return 0;
|
|
1266
|
+
const current = this.#settings.get(setting) as CodexContextTier;
|
|
1267
|
+
const index = this.#contextActions(model).findIndex(action => action.tier === current);
|
|
1268
|
+
return index >= 0 ? index : 0;
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1200
1271
|
#getSelectedItem(): ModelItem | CanonicalModelItem | undefined {
|
|
1201
1272
|
return this.#isCanonicalTab()
|
|
1202
1273
|
? this.#filteredCanonicalModels[this.#selectedIndex]
|
|
@@ -1214,29 +1285,31 @@ export class ModelSelectorComponent extends Container {
|
|
|
1214
1285
|
if (!this.#menuItem || this.#isItemDisabled(this.#menuItem)) return;
|
|
1215
1286
|
this.#isMenuOpen = true;
|
|
1216
1287
|
this.#menuScope = "conversation";
|
|
1217
|
-
this.#menuStep = "
|
|
1288
|
+
this.#menuStep = "thinking";
|
|
1218
1289
|
this.#menuSelectedRole = null;
|
|
1219
|
-
this.#
|
|
1290
|
+
this.#menuThinkingLevel = this.#getThinkingLevelsForModel(this.#menuItem.model)[0] ?? ThinkingLevel.Inherit;
|
|
1291
|
+
this.#menuContextTier = undefined;
|
|
1292
|
+
this.#menuSelectedIndex = this.#getThinkingPreselectIndex("default", this.#menuItem.model);
|
|
1220
1293
|
this.#updateMenu();
|
|
1221
1294
|
}
|
|
1222
1295
|
|
|
1223
|
-
#
|
|
1296
|
+
#openScopeMenu(): void {
|
|
1224
1297
|
const selected = this.#menuItem ?? this.#getSelectedItem();
|
|
1225
1298
|
if (!selected) return;
|
|
1226
1299
|
this.#menuItem = selected;
|
|
1227
1300
|
this.#isMenuOpen = true;
|
|
1228
|
-
this.#menuStep = "
|
|
1229
|
-
this.#
|
|
1230
|
-
this.#menuSelectedIndex = this.#getThinkingPreselectIndex(role, selected.model);
|
|
1301
|
+
this.#menuStep = "scope";
|
|
1302
|
+
this.#menuSelectedIndex = 0;
|
|
1231
1303
|
this.#updateMenu();
|
|
1232
1304
|
}
|
|
1233
1305
|
|
|
1234
1306
|
#closeMenu(): void {
|
|
1235
1307
|
this.#resetDetailPage();
|
|
1236
1308
|
this.#isMenuOpen = false;
|
|
1237
|
-
this.#menuStep = "
|
|
1309
|
+
this.#menuStep = "thinking";
|
|
1238
1310
|
this.#menuSelectedRole = null;
|
|
1239
1311
|
this.#menuItem = undefined;
|
|
1312
|
+
this.#menuContextTier = undefined;
|
|
1240
1313
|
this.#updateList();
|
|
1241
1314
|
}
|
|
1242
1315
|
|
|
@@ -1336,11 +1409,13 @@ export class ModelSelectorComponent extends Container {
|
|
|
1336
1409
|
if (!selectedItem) return;
|
|
1337
1410
|
|
|
1338
1411
|
const optionCount =
|
|
1339
|
-
this.#menuStep === "thinking"
|
|
1412
|
+
this.#menuStep === "thinking"
|
|
1340
1413
|
? this.#getThinkingLevelsForModel(selectedItem.model).length
|
|
1341
|
-
: this.#menuStep === "
|
|
1342
|
-
? this.#
|
|
1343
|
-
: this.#
|
|
1414
|
+
: this.#menuStep === "context"
|
|
1415
|
+
? this.#contextActions(selectedItem.model).length
|
|
1416
|
+
: this.#menuStep === "scope"
|
|
1417
|
+
? this.#scopeActions().length
|
|
1418
|
+
: this.#menuRoleActions.length;
|
|
1344
1419
|
if (optionCount === 0) return;
|
|
1345
1420
|
|
|
1346
1421
|
if (matchesSelectorKey(keyData, "up")) {
|
|
@@ -1356,13 +1431,31 @@ export class ModelSelectorComponent extends Container {
|
|
|
1356
1431
|
}
|
|
1357
1432
|
|
|
1358
1433
|
if (matchesSelectorKey(keyData, "confirm")) {
|
|
1434
|
+
if (this.#menuStep === "thinking") {
|
|
1435
|
+
const thinking = this.#getThinkingLevelsForModel(selectedItem.model)[this.#menuSelectedIndex];
|
|
1436
|
+
if (!thinking) return;
|
|
1437
|
+
this.#menuThinkingLevel = thinking;
|
|
1438
|
+
if (this.#contextActions(selectedItem.model).length > 0) {
|
|
1439
|
+
this.#menuStep = "context";
|
|
1440
|
+
this.#menuSelectedIndex = this.#contextPreselectIndex(selectedItem.model);
|
|
1441
|
+
this.#updateMenu();
|
|
1442
|
+
} else this.#openScopeMenu();
|
|
1443
|
+
return;
|
|
1444
|
+
}
|
|
1445
|
+
if (this.#menuStep === "context") {
|
|
1446
|
+
const context = this.#contextActions(selectedItem.model)[this.#menuSelectedIndex];
|
|
1447
|
+
if (!context) return;
|
|
1448
|
+
this.#menuContextTier = context.tier;
|
|
1449
|
+
this.#openScopeMenu();
|
|
1450
|
+
return;
|
|
1451
|
+
}
|
|
1359
1452
|
if (this.#menuStep === "scope") {
|
|
1360
1453
|
this.#menuScope = (["conversation", "default", "role"] as const)[this.#menuSelectedIndex];
|
|
1361
1454
|
if (this.#menuScope === "role") {
|
|
1362
1455
|
this.#menuStep = "role";
|
|
1363
1456
|
this.#menuSelectedIndex = 0;
|
|
1364
1457
|
this.#updateMenu();
|
|
1365
|
-
} else this.#
|
|
1458
|
+
} else void this.#handleSelect(selectedItem, this.#menuThinkingLevel);
|
|
1366
1459
|
return;
|
|
1367
1460
|
}
|
|
1368
1461
|
if (this.#menuStep === "role") {
|
|
@@ -1370,27 +1463,41 @@ export class ModelSelectorComponent extends Container {
|
|
|
1370
1463
|
const action = this.#menuRoleActions[this.#menuSelectedIndex];
|
|
1371
1464
|
if (!action) return;
|
|
1372
1465
|
this.#menuSelectedRole = action.role;
|
|
1373
|
-
this.#
|
|
1374
|
-
this.#menuSelectedIndex = this.#getThinkingPreselectIndex(action.role, selectedItem.model);
|
|
1375
|
-
this.#updateMenu();
|
|
1466
|
+
void this.#handleSelect(selectedItem, this.#menuThinkingLevel);
|
|
1376
1467
|
return;
|
|
1377
1468
|
}
|
|
1378
|
-
|
|
1379
|
-
if (!this.#menuSelectedRole) return;
|
|
1380
|
-
const thinkingOptions = this.#getThinkingLevelsForModel(selectedItem.model);
|
|
1381
|
-
const thinkingLevel = thinkingOptions[this.#menuSelectedIndex];
|
|
1382
|
-
if (!thinkingLevel) return;
|
|
1383
|
-
void this.#handleSelect(selectedItem, thinkingLevel);
|
|
1384
|
-
return;
|
|
1385
1469
|
}
|
|
1386
1470
|
|
|
1387
1471
|
if (getKeybindings().matches(keyData, "tui.select.cancel")) {
|
|
1388
|
-
if (this.#menuStep === "
|
|
1389
|
-
this.#menuStep =
|
|
1390
|
-
const roleIndex = this.#menuRoleActions.findIndex(action => action.role === this.#menuSelectedRole);
|
|
1472
|
+
if (this.#menuStep === "role") {
|
|
1473
|
+
this.#menuStep = "scope";
|
|
1391
1474
|
this.#menuSelectedRole = null;
|
|
1392
|
-
this.#menuSelectedIndex =
|
|
1393
|
-
|
|
1475
|
+
this.#menuSelectedIndex = 2;
|
|
1476
|
+
this.#updateMenu();
|
|
1477
|
+
return;
|
|
1478
|
+
}
|
|
1479
|
+
if (this.#menuStep === "scope") {
|
|
1480
|
+
if (this.#contextActions(selectedItem.model).length > 0) {
|
|
1481
|
+
this.#menuStep = "context";
|
|
1482
|
+
this.#menuSelectedIndex = this.#contextActions(selectedItem.model).findIndex(
|
|
1483
|
+
action => action.tier === this.#menuContextTier,
|
|
1484
|
+
);
|
|
1485
|
+
if (this.#menuSelectedIndex < 0)
|
|
1486
|
+
this.#menuSelectedIndex = this.#contextPreselectIndex(selectedItem.model);
|
|
1487
|
+
} else {
|
|
1488
|
+
this.#menuStep = "thinking";
|
|
1489
|
+
this.#menuSelectedIndex = this.#getThinkingLevelsForModel(selectedItem.model).indexOf(
|
|
1490
|
+
this.#menuThinkingLevel,
|
|
1491
|
+
);
|
|
1492
|
+
}
|
|
1493
|
+
this.#updateMenu();
|
|
1494
|
+
return;
|
|
1495
|
+
}
|
|
1496
|
+
if (this.#menuStep === "context") {
|
|
1497
|
+
this.#menuStep = "thinking";
|
|
1498
|
+
this.#menuSelectedIndex = this.#getThinkingLevelsForModel(selectedItem.model).indexOf(
|
|
1499
|
+
this.#menuThinkingLevel,
|
|
1500
|
+
);
|
|
1394
1501
|
this.#updateMenu();
|
|
1395
1502
|
return;
|
|
1396
1503
|
}
|
|
@@ -1411,6 +1518,7 @@ export class ModelSelectorComponent extends Container {
|
|
|
1411
1518
|
model: item.model,
|
|
1412
1519
|
selector: item.selector,
|
|
1413
1520
|
thinkingLevel,
|
|
1521
|
+
...(this.#menuContextTier ? { contextTier: this.#menuContextTier } : {}),
|
|
1414
1522
|
...(this.#menuScope === "role" ? { role: this.#menuSelectedRole! } : {}),
|
|
1415
1523
|
};
|
|
1416
1524
|
this.#applying = true;
|
|
@@ -17,8 +17,21 @@ export function prepareModelSelection(session: AgentSession, selection: ModelSel
|
|
|
17
17
|
: "None";
|
|
18
18
|
const selectedModel = formatModelSelectorValue(selection.selector, selection.thinkingLevel);
|
|
19
19
|
const role = selection.scope === "default" ? "default" : selection.role;
|
|
20
|
+
const contextSetting =
|
|
21
|
+
selection.contextTier && model.provider === "openai-codex"
|
|
22
|
+
? "providers.openaiContextTier"
|
|
23
|
+
: selection.contextTier && model.provider === "litellm"
|
|
24
|
+
? "providers.litellmContextTier"
|
|
25
|
+
: undefined;
|
|
20
26
|
const routingState = session.getRoutingState();
|
|
21
27
|
const changes: Array<{ field: string; before: string; after: string }> = [];
|
|
28
|
+
if (contextSetting) {
|
|
29
|
+
changes.push({
|
|
30
|
+
field: "Provider-wide context tier",
|
|
31
|
+
before: String(session.settings.get(contextSetting)),
|
|
32
|
+
after: selection.contextTier!,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
22
35
|
if (selection.scope !== "conversation") {
|
|
23
36
|
if (!role) return undefined;
|
|
24
37
|
changes.push({
|
|
@@ -56,7 +69,7 @@ export function prepareModelSelection(session: AgentSession, selection: ModelSel
|
|
|
56
69
|
changes: changed,
|
|
57
70
|
consequence:
|
|
58
71
|
selection.scope === "conversation"
|
|
59
|
-
? "Switches this conversation and records a manual routing pin;
|
|
72
|
+
? "Switches this conversation and records a manual routing pin; the selected provider-wide context tier is saved."
|
|
60
73
|
: selection.scope === "default"
|
|
61
74
|
? "Saves the user default role, then switches this conversation and records a manual routing pin."
|
|
62
75
|
: `Saves the ${role} role for future routed work; the active conversation model is unchanged.`,
|
|
@@ -71,6 +84,13 @@ export async function applyModelSelection(session: AgentSession, selection: Mode
|
|
|
71
84
|
const previousModel = session.model;
|
|
72
85
|
const previousThinking = session.thinkingLevel;
|
|
73
86
|
const previousRoles = { ...session.settings.get("modelRoles") };
|
|
87
|
+
const contextSetting =
|
|
88
|
+
selection.contextTier && model.provider === "openai-codex"
|
|
89
|
+
? "providers.openaiContextTier"
|
|
90
|
+
: selection.contextTier && model.provider === "litellm"
|
|
91
|
+
? "providers.litellmContextTier"
|
|
92
|
+
: undefined;
|
|
93
|
+
const previousContextTier = contextSetting ? session.settings.get(contextSetting) : undefined;
|
|
74
94
|
const previousRouting = session.getRoutingState();
|
|
75
95
|
let switched = false;
|
|
76
96
|
const role = scope === "default" ? "default" : selection.role;
|
|
@@ -80,10 +100,16 @@ export async function applyModelSelection(session: AgentSession, selection: Mode
|
|
|
80
100
|
throw new Error(`No API key for ${selector}`);
|
|
81
101
|
}
|
|
82
102
|
try {
|
|
83
|
-
if (scope !== "conversation") {
|
|
84
|
-
|
|
103
|
+
if (scope !== "conversation" || contextSetting) {
|
|
104
|
+
if (scope !== "conversation")
|
|
105
|
+
session.settings.setModelRole(role!, formatModelSelectorValue(selector, thinkingLevel));
|
|
106
|
+
if (contextSetting) session.settings.set(contextSetting, selection.contextTier!);
|
|
85
107
|
await session.settings.flush({ throwOnError: true });
|
|
86
108
|
}
|
|
109
|
+
if (contextSetting) {
|
|
110
|
+
if (model.provider === "openai-codex") session.modelRegistry.setOpenAIContextTier(selection.contextTier!);
|
|
111
|
+
else session.modelRegistry.setLiteLLMContextTier(selection.contextTier!);
|
|
112
|
+
}
|
|
87
113
|
if (scope !== "role") {
|
|
88
114
|
switched = true;
|
|
89
115
|
await session.setModelTemporary(model, thinkingLevel);
|
|
@@ -101,10 +127,15 @@ export async function applyModelSelection(session: AgentSession, selection: Mode
|
|
|
101
127
|
await session.sessionManager.flush();
|
|
102
128
|
}
|
|
103
129
|
} catch (error) {
|
|
104
|
-
if (scope !== "conversation") {
|
|
130
|
+
if (scope !== "conversation" || contextSetting) {
|
|
105
131
|
session.settings.set("modelRoles", previousRoles);
|
|
132
|
+
if (contextSetting) session.settings.set(contextSetting, previousContextTier!);
|
|
106
133
|
await session.settings.flush({ throwOnError: true }).catch(() => {});
|
|
107
134
|
}
|
|
135
|
+
if (contextSetting) {
|
|
136
|
+
if (model.provider === "openai-codex") session.modelRegistry.setOpenAIContextTier(previousContextTier!);
|
|
137
|
+
else session.modelRegistry.setLiteLLMContextTier(previousContextTier!);
|
|
138
|
+
}
|
|
108
139
|
if (switched && previousModel) await session.setModelTemporary(previousModel, previousThinking);
|
|
109
140
|
session.restoreRoutingState(previousRouting);
|
|
110
141
|
if (switched) {
|
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
For conceptual, product, operational, and support questions, search `xcsh://documentation/?search=<query>` first.
|
|
4
4
|
Select the best result and read its exact follow-up URL,
|
|
5
5
|
`xcsh://documentation/<source>/<stable-path>/index.md`, before answering. Use the source filter when the question
|
|
6
|
-
clearly targets `docs-cloud-f5-com` or `
|
|
6
|
+
clearly targets `docs-cloud-f5-com`, `my-f5-com`, or `www-f5-com`. Prefer `www-f5-com` for conceptual and product
|
|
7
|
+
questions, `docs-cloud-f5-com` for configuration and procedures, and `my-f5-com` for troubleshooting and support.
|
|
8
|
+
An explicit source filter is authoritative.
|
|
7
9
|
|
|
8
10
|
Render documentation images only when the user explicitly asks to see them. Extract all unique Markdown image references
|
|
9
11
|
from the exact document, resolve relative references against its exact `xcsh://documentation/` URI, and
|
|
@@ -494,6 +494,8 @@ For conceptual, product, operational, or support questions about F5 Distributed
|
|
|
494
494
|
search `xcsh://documentation/?search=<query>` first, then read the exact
|
|
495
495
|
`xcsh://documentation/<source>/<stable-path>/index.md` result before answering. The search result
|
|
496
496
|
is discovery; the exact Markdown document is the evidence.
|
|
497
|
+
Prefer `www-f5-com` for conceptual and product questions, `docs-cloud-f5-com` for configuration and
|
|
498
|
+
procedural questions, and `my-f5-com` for troubleshooting and support. An explicit source filter is authoritative.
|
|
497
499
|
|
|
498
500
|
### Documentation images
|
|
499
501
|
|
package/src/qmd-smoke.ts
CHANGED
|
@@ -6,7 +6,11 @@ import {
|
|
|
6
6
|
createEmbeddedDocumentationRepository,
|
|
7
7
|
type EmbeddedDocumentationRepository,
|
|
8
8
|
} from "./internal-urls/documentation-repository";
|
|
9
|
-
import type {
|
|
9
|
+
import type {
|
|
10
|
+
DocumentationRepository,
|
|
11
|
+
DocumentationSearchResult,
|
|
12
|
+
DocumentationSource,
|
|
13
|
+
} from "./internal-urls/documentation-resolve";
|
|
10
14
|
import { convertToPng } from "./utils/image-convert";
|
|
11
15
|
|
|
12
16
|
export const QMD_SMOKE_SUCCESS = "XCSH_QMD_SMOKE_OK";
|
|
@@ -17,6 +21,10 @@ const EXPECTED_CATEGORY = "dns-dns-zone-clone-from-dns-domain";
|
|
|
17
21
|
const WAF_QUERY = "configure web application firewall";
|
|
18
22
|
const WAF_PATH = "docs/how-to/app-security/web-app-firewall";
|
|
19
23
|
const WAF_TITLE = "Create Web Application Firewall";
|
|
24
|
+
const MARKETING_QUERY = "what is client side defense";
|
|
25
|
+
const MARKETING_PATH = "products/distributed-cloud-services/client-side-defense";
|
|
26
|
+
const MARKETING_TITLE = "F5 Distributed Cloud Client-Side Defense";
|
|
27
|
+
const MARKETING_SOURCE = "www-f5-com" as const;
|
|
20
28
|
const DNS_QUERY = "set up DNS load balancer";
|
|
21
29
|
const DNS_PATH = "dns-management/how-to/configure-dns-load-balancer";
|
|
22
30
|
const DNS_TITLE = "Set Up DNS Load Balancer";
|
|
@@ -31,6 +39,8 @@ const EXPECTED_EVENTS = [
|
|
|
31
39
|
"api-catalog-rank",
|
|
32
40
|
"documentation-search",
|
|
33
41
|
"documentation-read",
|
|
42
|
+
"documentation-marketing-search",
|
|
43
|
+
"documentation-marketing-read",
|
|
34
44
|
"documentation-search",
|
|
35
45
|
"documentation-read",
|
|
36
46
|
"documentation-follow-up-read",
|
|
@@ -47,7 +57,7 @@ export interface QmdSmokeTraceEntry {
|
|
|
47
57
|
readonly event: QmdSmokeEvent;
|
|
48
58
|
readonly resource?: string;
|
|
49
59
|
readonly title?: string;
|
|
50
|
-
readonly source?:
|
|
60
|
+
readonly source?: DocumentationSource;
|
|
51
61
|
readonly category?: string;
|
|
52
62
|
readonly outcome?: string;
|
|
53
63
|
}
|
|
@@ -59,12 +69,12 @@ export interface QmdSmokeOptions {
|
|
|
59
69
|
documentationRepository?: DocumentationRepository;
|
|
60
70
|
}
|
|
61
71
|
|
|
62
|
-
function searchUri(query: string): string {
|
|
63
|
-
return `xcsh://documentation/?search=${encodeURIComponent(query)}&source=${
|
|
72
|
+
function searchUri(query: string, source: DocumentationSource = SOURCE): string {
|
|
73
|
+
return `xcsh://documentation/?search=${encodeURIComponent(query)}&source=${source}&limit=1`;
|
|
64
74
|
}
|
|
65
75
|
|
|
66
|
-
function documentUri(stablePath: string): string {
|
|
67
|
-
return `xcsh://documentation/${
|
|
76
|
+
function documentUri(stablePath: string, source: DocumentationSource = SOURCE): string {
|
|
77
|
+
return `xcsh://documentation/${source}/${stablePath}/index.md`;
|
|
68
78
|
}
|
|
69
79
|
|
|
70
80
|
function assetUri(stablePath: string, filename: string): string {
|
|
@@ -75,11 +85,12 @@ function assertSearchResult(
|
|
|
75
85
|
result: DocumentationSearchResult | undefined,
|
|
76
86
|
expectedPath: string,
|
|
77
87
|
expectedTitle: string,
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
88
|
+
expectedSource: DocumentationSource = SOURCE,
|
|
89
|
+
): DocumentationSearchResult {
|
|
90
|
+
if (result?.source !== expectedSource || result.stablePath !== expectedPath || result.title !== expectedTitle) {
|
|
91
|
+
throw new Error(`Documentation QMD smoke expected ${expectedTitle} at ${expectedSource}/${expectedPath}`);
|
|
81
92
|
}
|
|
82
|
-
return result
|
|
93
|
+
return result;
|
|
83
94
|
}
|
|
84
95
|
|
|
85
96
|
/** Parse and strictly validate stdout from the installed-binary QMD smoke driver. */
|
|
@@ -109,6 +120,8 @@ export function parseQmdSmokeOutput(output: string): QmdSmokeTraceEntry[] {
|
|
|
109
120
|
const expectedResources = [
|
|
110
121
|
searchUri(WAF_QUERY),
|
|
111
122
|
documentUri(WAF_PATH),
|
|
123
|
+
searchUri(MARKETING_QUERY, MARKETING_SOURCE),
|
|
124
|
+
documentUri(MARKETING_PATH, MARKETING_SOURCE),
|
|
112
125
|
searchUri(DNS_QUERY),
|
|
113
126
|
documentUri(DNS_PATH),
|
|
114
127
|
documentUri(DNS_PATH),
|
|
@@ -126,16 +139,20 @@ export function parseQmdSmokeOutput(output: string): QmdSmokeTraceEntry[] {
|
|
|
126
139
|
if (trace[index]?.title !== WAF_TITLE || trace[index]?.source !== SOURCE)
|
|
127
140
|
throw new Error("QMD smoke trace contains unexpected WAF identity");
|
|
128
141
|
}
|
|
129
|
-
for (const index of [3, 4
|
|
142
|
+
for (const index of [3, 4]) {
|
|
143
|
+
if (trace[index]?.title !== MARKETING_TITLE || trace[index]?.source !== MARKETING_SOURCE)
|
|
144
|
+
throw new Error("QMD smoke trace contains unexpected marketing identity");
|
|
145
|
+
}
|
|
146
|
+
for (const index of [5, 6, 7]) {
|
|
130
147
|
if (trace[index]?.title !== DNS_TITLE || trace[index]?.source !== SOURCE)
|
|
131
148
|
throw new Error("QMD smoke trace contains unexpected DNS identity");
|
|
132
149
|
}
|
|
133
|
-
if (trace[
|
|
134
|
-
if (trace[
|
|
150
|
+
if (trace[8]?.outcome !== "no-match-no-fetch") throw new Error("QMD smoke trace did not fail closed");
|
|
151
|
+
if (trace[9]?.outcome !== DOCUMENTATION_TOOLS_DISABLED_MESSAGE)
|
|
135
152
|
throw new Error("QMD smoke trace omitted the tools-disabled explanation");
|
|
136
|
-
if (trace[
|
|
153
|
+
if (trace[10]?.source !== SOURCE || trace[10].outcome !== "image/png")
|
|
137
154
|
throw new Error("QMD smoke trace contains an unexpected SVG conversion result");
|
|
138
|
-
if (trace[
|
|
155
|
+
if (trace[11]?.outcome !== "ok") throw new Error("QMD smoke trace did not confirm SQLite startup");
|
|
139
156
|
return trace;
|
|
140
157
|
}
|
|
141
158
|
|
|
@@ -173,6 +190,32 @@ export async function runQmdSmoke(options: QmdSmokeOptions = {}): Promise<string
|
|
|
173
190
|
}
|
|
174
191
|
emit({ event: "documentation-read", resource: documentUri(WAF_PATH), title: wafDocument.title, source: SOURCE });
|
|
175
192
|
|
|
193
|
+
const marketing = assertSearchResult(
|
|
194
|
+
(await documentation.search(MARKETING_QUERY, MARKETING_SOURCE, 1))[0],
|
|
195
|
+
MARKETING_PATH,
|
|
196
|
+
MARKETING_TITLE,
|
|
197
|
+
MARKETING_SOURCE,
|
|
198
|
+
);
|
|
199
|
+
emit({
|
|
200
|
+
event: "documentation-marketing-search",
|
|
201
|
+
resource: searchUri(MARKETING_QUERY, MARKETING_SOURCE),
|
|
202
|
+
title: marketing.title,
|
|
203
|
+
source: marketing.source,
|
|
204
|
+
});
|
|
205
|
+
const marketingDocument = await documentation.readDocument(MARKETING_SOURCE, MARKETING_PATH);
|
|
206
|
+
if (
|
|
207
|
+
marketingDocument?.title !== MARKETING_TITLE ||
|
|
208
|
+
!marketingDocument.markdown.includes("# F5 Client-Side Defense")
|
|
209
|
+
) {
|
|
210
|
+
throw new Error("Documentation QMD smoke could not read exact marketing Markdown");
|
|
211
|
+
}
|
|
212
|
+
emit({
|
|
213
|
+
event: "documentation-marketing-read",
|
|
214
|
+
resource: documentUri(MARKETING_PATH, MARKETING_SOURCE),
|
|
215
|
+
title: marketingDocument.title,
|
|
216
|
+
source: MARKETING_SOURCE,
|
|
217
|
+
});
|
|
218
|
+
|
|
176
219
|
const dns = assertSearchResult((await documentation.search(DNS_QUERY, SOURCE, 1))[0], DNS_PATH, DNS_TITLE);
|
|
177
220
|
emit({ event: "documentation-search", resource: searchUri(DNS_QUERY), title: dns.title, source: dns.source });
|
|
178
221
|
const dnsDocument = await documentation.readDocument(SOURCE, DNS_PATH);
|