advisor-pi 1.0.3 → 1.2.0
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/README.md +39 -2
- package/dist/config-ui.d.ts +12 -0
- package/dist/config-ui.d.ts.map +1 -0
- package/dist/config-ui.js +26 -0
- package/dist/config-ui.js.map +1 -0
- package/dist/index.d.ts +52 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +303 -53
- package/dist/index.js.map +1 -1
- package/dist/preferences.d.ts +40 -0
- package/dist/preferences.d.ts.map +1 -0
- package/dist/preferences.js +198 -0
- package/dist/preferences.js.map +1 -0
- package/package.json +2 -1
- package/src/config-ui.ts +40 -0
- package/src/index.ts +334 -52
- package/src/preferences.ts +228 -0
package/src/index.ts
CHANGED
|
@@ -7,16 +7,22 @@ import {
|
|
|
7
7
|
type ExtensionContext,
|
|
8
8
|
} from "@earendil-works/pi-coding-agent";
|
|
9
9
|
import { Type, type Static } from "typebox";
|
|
10
|
+
import { loadPreferences, savePreferencesPatch, type AdvisorPreferencesPatch } from "./preferences.js";
|
|
11
|
+
import { buildMenuRows, type MenuKey, rowForLabel } from "./config-ui.js";
|
|
10
12
|
|
|
11
|
-
|
|
13
|
+
export { loadPreferences, preferencesPath, savePreferences, savePreferencesPatch } from "./preferences.js";
|
|
14
|
+
export type { AdvisorPreferences, AdvisorPreferencesConfig, AdvisorPreferencesPatch } from "./preferences.js";
|
|
15
|
+
|
|
16
|
+
export const STATE_ENTRY = "advisor-pi-state";
|
|
12
17
|
const TOOL_NAME = "advisor";
|
|
13
|
-
const DEFAULT_ADVISOR_MODEL = "openai-codex/gpt-5.6-sol";
|
|
14
|
-
const LEGACY_DEFAULT_ADVISOR_MODEL = "openai-codex/gpt-5.5";
|
|
18
|
+
export const DEFAULT_ADVISOR_MODEL = "openai-codex/gpt-5.6-sol";
|
|
19
|
+
export const LEGACY_DEFAULT_ADVISOR_MODEL = "openai-codex/gpt-5.5";
|
|
15
20
|
const DEFAULT_ADVISOR_THINKING_LEVEL: AdvisorThinkingLevel = "high";
|
|
16
21
|
const DEFAULT_MAX_USES = 5;
|
|
17
22
|
const DEFAULT_CACHE_RETENTION: CacheRetention = "short";
|
|
18
23
|
const DEFAULT_TIMEOUT_MS = 600_000;
|
|
19
24
|
const DEFAULT_MAX_TOKENS = 4_000;
|
|
25
|
+
export const DEFAULT_MAX_TRANSCRIPT_CHARS = 20_000;
|
|
20
26
|
|
|
21
27
|
const advisorToolSchema = Type.Object({
|
|
22
28
|
question: Type.String({
|
|
@@ -40,7 +46,9 @@ const advisorToolSchema = Type.Object({
|
|
|
40
46
|
type AdvisorToolInput = Static<typeof advisorToolSchema>;
|
|
41
47
|
type AdvisorThinkingLevel = ThinkingLevel | "max";
|
|
42
48
|
|
|
43
|
-
type
|
|
49
|
+
type ModeAwareContext = ExtensionContext & { mode?: string };
|
|
50
|
+
|
|
51
|
+
export type AdvisorConfig = {
|
|
44
52
|
enabled: boolean;
|
|
45
53
|
provider: string;
|
|
46
54
|
modelId: string;
|
|
@@ -49,6 +57,7 @@ type AdvisorConfig = {
|
|
|
49
57
|
cacheRetention: CacheRetention;
|
|
50
58
|
maxTokens: number;
|
|
51
59
|
timeoutMs: number;
|
|
60
|
+
maxTranscriptChars: number;
|
|
52
61
|
};
|
|
53
62
|
|
|
54
63
|
type AdvisorStateEntry = {
|
|
@@ -74,8 +83,21 @@ type AdvisorToolDetails = {
|
|
|
74
83
|
state: AdvisorStateEntry;
|
|
75
84
|
};
|
|
76
85
|
|
|
86
|
+
export type ModelRegistryLike = {
|
|
87
|
+
find: (provider: string, modelId: string) => unknown;
|
|
88
|
+
getAvailable?: () => Array<{ provider?: unknown; id?: unknown }>;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
export type NormalizeConfigOptions = {
|
|
92
|
+
migrateLegacyDefault?: boolean;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
function isTuiContext(ctx: ExtensionContext): boolean {
|
|
96
|
+
return (ctx as ModeAwareContext).mode === "tui" && ctx.hasUI;
|
|
97
|
+
}
|
|
98
|
+
|
|
77
99
|
export default function advisorPiExtension(pi: ExtensionAPI) {
|
|
78
|
-
let config = defaultConfig();
|
|
100
|
+
let config = normalizeConfig(loadPreferences(), defaultConfig(), undefined, { migrateLegacyDefault: false });
|
|
79
101
|
let useCount = 0;
|
|
80
102
|
|
|
81
103
|
pi.registerFlag("advisor-model", {
|
|
@@ -94,10 +116,13 @@ export default function advisorPiExtension(pi: ExtensionAPI) {
|
|
|
94
116
|
description: "Advisor prompt-cache preference: none, short, or long",
|
|
95
117
|
type: "string",
|
|
96
118
|
});
|
|
119
|
+
pi.registerFlag("advisor-max-transcript-chars", {
|
|
120
|
+
description: "Maximum conversation transcript characters sent per advisor call (keeps most recent)",
|
|
121
|
+
type: "string",
|
|
122
|
+
});
|
|
97
123
|
pi.registerFlag("advisor-enabled", {
|
|
98
124
|
description: "Enable the advisor tool on startup",
|
|
99
125
|
type: "boolean",
|
|
100
|
-
default: true,
|
|
101
126
|
});
|
|
102
127
|
|
|
103
128
|
pi.registerTool<typeof advisorToolSchema, AdvisorToolDetails>({
|
|
@@ -115,8 +140,8 @@ export default function advisorPiExtension(pi: ExtensionAPI) {
|
|
|
115
140
|
parameters: advisorToolSchema,
|
|
116
141
|
executionMode: "sequential",
|
|
117
142
|
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
|
118
|
-
|
|
119
|
-
|
|
143
|
+
// Branch state is replayed at session_start/session_tree; refreshing here
|
|
144
|
+
// would reapply startup flags over explicit in-session command edits.
|
|
120
145
|
if (!config.enabled) {
|
|
121
146
|
return advisorDisabledResult(config, useCount, params);
|
|
122
147
|
}
|
|
@@ -146,7 +171,7 @@ export default function advisorPiExtension(pi: ExtensionAPI) {
|
|
|
146
171
|
if (!auth.ok) throw new Error(auth.error);
|
|
147
172
|
|
|
148
173
|
const startedAt = Date.now();
|
|
149
|
-
const conversationText = serializeCurrentConversation(ctx);
|
|
174
|
+
const conversationText = truncateTranscript(serializeCurrentConversation(ctx), config.maxTranscriptChars);
|
|
150
175
|
const userMessage: Message = {
|
|
151
176
|
role: "user",
|
|
152
177
|
content: [
|
|
@@ -218,13 +243,16 @@ export default function advisorPiExtension(pi: ExtensionAPI) {
|
|
|
218
243
|
});
|
|
219
244
|
|
|
220
245
|
pi.registerCommand("advisor-pi", {
|
|
221
|
-
description: "Configure advisor-pi: status, enable, disable, model, thinking, max-uses, cache, reset",
|
|
246
|
+
description: "Configure advisor-pi: status, enable, disable, model, thinking, max-uses, max-transcript-chars, cache, reset",
|
|
222
247
|
handler: async (args, ctx) => {
|
|
223
|
-
const
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
248
|
+
const trimmed = args.trim();
|
|
249
|
+
// Bare /advisor-pi is interactive only in the terminal TUI. Other modes
|
|
250
|
+
// keep the existing status response for scripts and print output.
|
|
251
|
+
if (!trimmed && isTuiContext(ctx)) {
|
|
252
|
+
await openConfigPanel(ctx);
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
await applyCommand(trimmed, ctx);
|
|
228
256
|
},
|
|
229
257
|
});
|
|
230
258
|
|
|
@@ -248,28 +276,30 @@ export default function advisorPiExtension(pi: ExtensionAPI) {
|
|
|
248
276
|
});
|
|
249
277
|
|
|
250
278
|
function refreshStateFromBranch(ctx: ExtensionContext): void {
|
|
251
|
-
config = defaultConfig();
|
|
279
|
+
config = normalizeConfig(loadPreferences(), defaultConfig(), ctx.modelRegistry, { migrateLegacyDefault: false });
|
|
280
|
+
|
|
252
281
|
useCount = 0;
|
|
253
|
-
|
|
254
|
-
|
|
282
|
+
restoreStateFromSession(ctx, ctx.modelRegistry);
|
|
283
|
+
applyStartupFlags(pi, ctx.modelRegistry);
|
|
284
|
+
ensureAdvisorModelResolves(ctx);
|
|
255
285
|
syncActiveTool(pi);
|
|
256
286
|
updateStatus(ctx);
|
|
257
287
|
}
|
|
258
288
|
|
|
259
|
-
function restoreStateFromSession(ctx: ExtensionContext): boolean {
|
|
289
|
+
function restoreStateFromSession(ctx: ExtensionContext, registry?: ModelRegistryLike): boolean {
|
|
260
290
|
let restored = false;
|
|
261
291
|
const branch = ctx.sessionManager.getBranch();
|
|
262
292
|
for (const entry of branch) {
|
|
263
293
|
if (entry.type === "custom" && entry.customType === STATE_ENTRY) {
|
|
264
294
|
restored = true;
|
|
265
295
|
const data = entry.data as Partial<AdvisorStateEntry> | undefined;
|
|
266
|
-
if (data?.config) config = normalizeConfig(data.config, config);
|
|
296
|
+
if (data?.config) config = normalizeConfig(data.config, config, registry);
|
|
267
297
|
if (typeof data?.useCount === "number") useCount = Math.max(0, data.useCount);
|
|
268
298
|
}
|
|
269
299
|
if (entry.type === "message" && entry.message.role === "toolResult" && entry.message.toolName === TOOL_NAME) {
|
|
270
300
|
restored = true;
|
|
271
301
|
const details = entry.message.details as Partial<AdvisorToolDetails> | undefined;
|
|
272
|
-
if (details?.state?.config) config = normalizeConfig(details.state.config, config);
|
|
302
|
+
if (details?.state?.config) config = normalizeConfig(details.state.config, config, registry);
|
|
273
303
|
if (typeof details?.state?.useCount === "number") useCount = Math.max(useCount, details.state.useCount);
|
|
274
304
|
else if (details?.advisor?.useCount) useCount = Math.max(useCount, details.advisor.useCount);
|
|
275
305
|
}
|
|
@@ -277,42 +307,78 @@ export default function advisorPiExtension(pi: ExtensionAPI) {
|
|
|
277
307
|
return restored;
|
|
278
308
|
}
|
|
279
309
|
|
|
280
|
-
function applyStartupFlags(api: ExtensionAPI): void {
|
|
281
|
-
const enabledFlag = api
|
|
310
|
+
function applyStartupFlags(api: ExtensionAPI, registry?: ModelRegistryLike): void {
|
|
311
|
+
const enabledFlag = flagValue(api, "advisor-enabled");
|
|
282
312
|
if (typeof enabledFlag === "boolean") config.enabled = enabledFlag;
|
|
283
313
|
|
|
284
|
-
const modelFlag = api
|
|
314
|
+
const modelFlag = flagValue(api, "advisor-model");
|
|
285
315
|
if (typeof modelFlag === "string" && modelFlag.trim()) {
|
|
286
316
|
const parsed = parseModelSpec(modelFlag.trim());
|
|
287
|
-
if (parsed) {
|
|
317
|
+
if (parsed && (!registry || modelResolves(registry, parsed.provider, parsed.modelId))) {
|
|
288
318
|
config.provider = parsed.provider;
|
|
289
319
|
config.modelId = parsed.modelId;
|
|
290
320
|
}
|
|
291
321
|
}
|
|
292
322
|
|
|
293
|
-
const thinkingFlag = api
|
|
323
|
+
const thinkingFlag = flagValue(api, "advisor-thinking");
|
|
294
324
|
if (typeof thinkingFlag === "string") {
|
|
295
325
|
const thinkingLevel = parseThinkingLevel(thinkingFlag);
|
|
296
326
|
if (thinkingLevel) config.thinkingLevel = thinkingLevel;
|
|
297
327
|
}
|
|
298
328
|
|
|
299
|
-
const maxUsesFlag = api
|
|
329
|
+
const maxUsesFlag = flagValue(api, "advisor-max-uses");
|
|
300
330
|
if (typeof maxUsesFlag === "string") {
|
|
301
331
|
const maxUses = parsePositiveInt(maxUsesFlag);
|
|
302
332
|
if (maxUses !== undefined) config.maxUses = maxUses;
|
|
303
333
|
}
|
|
304
334
|
|
|
305
|
-
const cacheFlag = api
|
|
335
|
+
const cacheFlag = flagValue(api, "advisor-cache");
|
|
306
336
|
if (typeof cacheFlag === "string") {
|
|
307
337
|
const cacheRetention = parseCacheRetention(cacheFlag);
|
|
308
338
|
if (cacheRetention) config.cacheRetention = cacheRetention;
|
|
309
339
|
}
|
|
340
|
+
|
|
341
|
+
const maxTranscriptCharsFlag = flagValue(api, "advisor-max-transcript-chars");
|
|
342
|
+
if (typeof maxTranscriptCharsFlag === "string") {
|
|
343
|
+
const maxTranscriptChars = parsePositiveInt(maxTranscriptCharsFlag);
|
|
344
|
+
if (maxTranscriptChars !== undefined) config.maxTranscriptChars = maxTranscriptChars;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function ensureAdvisorModelResolves(ctx: ExtensionContext): void {
|
|
349
|
+
const registry = ctx.modelRegistry as ModelRegistryLike | undefined;
|
|
350
|
+
if (!registry || modelResolves(registry, config.provider, config.modelId)) return;
|
|
351
|
+
const previous = `${config.provider}/${config.modelId}`;
|
|
352
|
+
const candidates = [parseModelSpec(DEFAULT_ADVISOR_MODEL), parseModelSpec(LEGACY_DEFAULT_ADVISOR_MODEL)].filter(
|
|
353
|
+
(candidate): candidate is { provider: string; modelId: string } => candidate !== undefined,
|
|
354
|
+
);
|
|
355
|
+
const working = resolveAdvisorModel(registry, candidates);
|
|
356
|
+
if (!working) return;
|
|
357
|
+
config.provider = working.provider;
|
|
358
|
+
config.modelId = working.modelId;
|
|
359
|
+
persistState(pi, config, useCount);
|
|
360
|
+
if (ctx.hasUI) {
|
|
361
|
+
ctx.ui.notify(`Advisor model ${previous} not found. Falling back to ${working.provider}/${working.modelId}.`, "warning");
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
async function persistPreferencesForEdit(ctx: ExtensionContext, patch: AdvisorPreferencesPatch): Promise<void> {
|
|
366
|
+
if (await savePreferencesPatch(patch)) return;
|
|
367
|
+
if (ctx.hasUI) {
|
|
368
|
+
ctx.ui.notify("Could not save advisor-pi preferences; this change applies to the current session only.", "warning");
|
|
369
|
+
}
|
|
310
370
|
}
|
|
311
371
|
|
|
312
372
|
function handleCommand(
|
|
313
373
|
args: string,
|
|
314
374
|
ctx: ExtensionContext,
|
|
315
|
-
): {
|
|
375
|
+
): {
|
|
376
|
+
message: string;
|
|
377
|
+
level: "info" | "warning" | "error";
|
|
378
|
+
persist: boolean;
|
|
379
|
+
preferencePatch?: AdvisorPreferencesPatch;
|
|
380
|
+
updateToolState: boolean;
|
|
381
|
+
} {
|
|
316
382
|
if (!args || args === "status") {
|
|
317
383
|
return {
|
|
318
384
|
message: formatStatus(ctx),
|
|
@@ -328,10 +394,22 @@ export default function advisorPiExtension(pi: ExtensionAPI) {
|
|
|
328
394
|
switch (command) {
|
|
329
395
|
case "enable":
|
|
330
396
|
config.enabled = true;
|
|
331
|
-
return {
|
|
397
|
+
return {
|
|
398
|
+
message: "advisor-pi enabled",
|
|
399
|
+
level: "info",
|
|
400
|
+
persist: true,
|
|
401
|
+
preferencePatch: { enabled: true },
|
|
402
|
+
updateToolState: true,
|
|
403
|
+
};
|
|
332
404
|
case "disable":
|
|
333
405
|
config.enabled = false;
|
|
334
|
-
return {
|
|
406
|
+
return {
|
|
407
|
+
message: "advisor-pi disabled",
|
|
408
|
+
level: "info",
|
|
409
|
+
persist: true,
|
|
410
|
+
preferencePatch: { enabled: false },
|
|
411
|
+
updateToolState: true,
|
|
412
|
+
};
|
|
335
413
|
case "reset":
|
|
336
414
|
useCount = 0;
|
|
337
415
|
return { message: "advisor-pi use count reset", level: "info", persist: true, updateToolState: false };
|
|
@@ -360,6 +438,7 @@ export default function advisorPiExtension(pi: ExtensionAPI) {
|
|
|
360
438
|
message: `advisor-pi model set to ${config.provider}/${config.modelId}`,
|
|
361
439
|
level: "info",
|
|
362
440
|
persist: true,
|
|
441
|
+
preferencePatch: { provider: config.provider, modelId: config.modelId },
|
|
363
442
|
updateToolState: false,
|
|
364
443
|
};
|
|
365
444
|
}
|
|
@@ -378,6 +457,7 @@ export default function advisorPiExtension(pi: ExtensionAPI) {
|
|
|
378
457
|
message: `advisor-pi thinking level set to ${thinkingLevel}`,
|
|
379
458
|
level: "info",
|
|
380
459
|
persist: true,
|
|
460
|
+
preferencePatch: { thinkingLevel },
|
|
381
461
|
updateToolState: false,
|
|
382
462
|
};
|
|
383
463
|
}
|
|
@@ -396,6 +476,7 @@ export default function advisorPiExtension(pi: ExtensionAPI) {
|
|
|
396
476
|
message: `advisor-pi max uses set to ${maxUses}`,
|
|
397
477
|
level: "info",
|
|
398
478
|
persist: true,
|
|
479
|
+
preferencePatch: { maxUses },
|
|
399
480
|
updateToolState: false,
|
|
400
481
|
};
|
|
401
482
|
}
|
|
@@ -414,13 +495,33 @@ export default function advisorPiExtension(pi: ExtensionAPI) {
|
|
|
414
495
|
message: `advisor-pi cache set to ${cacheRetention}`,
|
|
415
496
|
level: "info",
|
|
416
497
|
persist: true,
|
|
498
|
+
preferencePatch: { cacheRetention },
|
|
499
|
+
updateToolState: false,
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
case "max-transcript-chars": {
|
|
503
|
+
const maxTranscriptChars = parsePositiveInt(value);
|
|
504
|
+
if (maxTranscriptChars === undefined) {
|
|
505
|
+
return {
|
|
506
|
+
message: "Usage: /advisor-pi max-transcript-chars <positive-number>",
|
|
507
|
+
level: "error",
|
|
508
|
+
persist: false,
|
|
509
|
+
updateToolState: false,
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
config.maxTranscriptChars = maxTranscriptChars;
|
|
513
|
+
return {
|
|
514
|
+
message: `advisor-pi max transcript chars set to ${maxTranscriptChars}`,
|
|
515
|
+
level: "info",
|
|
516
|
+
persist: true,
|
|
517
|
+
preferencePatch: { maxTranscriptChars },
|
|
417
518
|
updateToolState: false,
|
|
418
519
|
};
|
|
419
520
|
}
|
|
420
521
|
default:
|
|
421
522
|
return {
|
|
422
523
|
message:
|
|
423
|
-
"Usage: /advisor-pi [status|enable|disable|model <provider>/<model>|thinking <minimal|low|medium|high|xhigh|max>|max-uses <n>|cache <none|short|long>|reset]",
|
|
524
|
+
"Usage: /advisor-pi [status|enable|disable|model <provider>/<model>|thinking <minimal|low|medium|high|xhigh|max>|max-uses <n>|max-transcript-chars <n>|cache <none|short|long>|reset]",
|
|
424
525
|
level: "error",
|
|
425
526
|
persist: false,
|
|
426
527
|
updateToolState: false,
|
|
@@ -428,6 +529,110 @@ export default function advisorPiExtension(pi: ExtensionAPI) {
|
|
|
428
529
|
}
|
|
429
530
|
}
|
|
430
531
|
|
|
532
|
+
async function applyCommand(
|
|
533
|
+
args: string,
|
|
534
|
+
ctx: ExtensionContext,
|
|
535
|
+
options: { redactModelErrors?: boolean } = {},
|
|
536
|
+
): Promise<void> {
|
|
537
|
+
const result = handleCommand(args, ctx);
|
|
538
|
+
if (result.persist) {
|
|
539
|
+
persistState(pi, config, useCount);
|
|
540
|
+
if (result.preferencePatch) await persistPreferencesForEdit(ctx, result.preferencePatch);
|
|
541
|
+
}
|
|
542
|
+
if (result.updateToolState) syncActiveTool(pi);
|
|
543
|
+
updateStatus(ctx);
|
|
544
|
+
const message = options.redactModelErrors && result.level === "error" ? "Advisor model not found" : result.message;
|
|
545
|
+
ctx.ui.notify(message, result.level);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
async function openConfigPanel(ctx: ExtensionContext): Promise<void> {
|
|
549
|
+
for (;;) {
|
|
550
|
+
const rows = buildMenuRows(config, useCount);
|
|
551
|
+
const choice = await ctx.ui.select("advisor-pi settings", rows.map((row) => row.label));
|
|
552
|
+
const row = rowForLabel(rows, choice);
|
|
553
|
+
// Escape returns undefined, which closes the panel without changing state.
|
|
554
|
+
if (!row || row.key === "close") return;
|
|
555
|
+
await applyMenuChoice(row.key, ctx);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
async function applyMenuChoice(key: MenuKey, ctx: ExtensionContext): Promise<void> {
|
|
560
|
+
switch (key) {
|
|
561
|
+
case "enabled": {
|
|
562
|
+
const choice = await ctx.ui.select("Advisor enabled", ["enabled", "disabled"]);
|
|
563
|
+
if (choice !== "enabled" && choice !== "disabled") return;
|
|
564
|
+
const enabled = choice === "enabled";
|
|
565
|
+
if (enabled === config.enabled) return;
|
|
566
|
+
const confirmed = await ctx.ui.confirm(
|
|
567
|
+
enabled ? "Enable advisor-pi?" : "Disable advisor-pi?",
|
|
568
|
+
enabled
|
|
569
|
+
? "The advisor tool will be available to the executor."
|
|
570
|
+
: "The advisor tool will be removed from the active tool list.",
|
|
571
|
+
);
|
|
572
|
+
if (!confirmed) return;
|
|
573
|
+
await applyCommand(enabled ? "enable" : "disable", ctx);
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
case "model": {
|
|
577
|
+
const typed = await ctx.ui.input("Advisor model", "provider/model");
|
|
578
|
+
if (typed === undefined) return;
|
|
579
|
+
const parsed = parseModelSpec(typed);
|
|
580
|
+
if (!parsed) {
|
|
581
|
+
ctx.ui.notify("Expected provider/model", "error");
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
let model: unknown;
|
|
585
|
+
try {
|
|
586
|
+
model = ctx.modelRegistry.find(parsed.provider, parsed.modelId);
|
|
587
|
+
} catch {
|
|
588
|
+
model = undefined;
|
|
589
|
+
}
|
|
590
|
+
if (!model) {
|
|
591
|
+
ctx.ui.notify("Advisor model not found", "error");
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
await applyCommand(`model ${parsed.provider}/${parsed.modelId}`, ctx, { redactModelErrors: true });
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
case "thinking": {
|
|
598
|
+
const choice = await ctx.ui.select("Advisor thinking", ["minimal", "low", "medium", "high", "xhigh", "max"]);
|
|
599
|
+
if (!parseThinkingLevel(choice)) return;
|
|
600
|
+
await applyCommand(`thinking ${choice}`, ctx);
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
case "max-uses": {
|
|
604
|
+
const value = await ctx.ui.input("Maximum advisor uses", String(config.maxUses));
|
|
605
|
+
if (value === undefined) return;
|
|
606
|
+
await applyCommand(`max-uses ${value}`, ctx);
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
case "max-transcript-chars": {
|
|
610
|
+
const value = await ctx.ui.input("Maximum transcript characters", String(config.maxTranscriptChars));
|
|
611
|
+
if (value === undefined) return;
|
|
612
|
+
await applyCommand(`max-transcript-chars ${value}`, ctx);
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
case "cache": {
|
|
616
|
+
const choice = await ctx.ui.select("Advisor cache retention", ["none", "short", "long"]);
|
|
617
|
+
if (!parseCacheRetention(choice)) return;
|
|
618
|
+
await applyCommand(`cache ${choice}`, ctx);
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
case "reset": {
|
|
622
|
+
if (useCount <= 0) return;
|
|
623
|
+
const confirmed = await ctx.ui.confirm(
|
|
624
|
+
"Reset advisor use count?",
|
|
625
|
+
`This clears ${useCount} advisor use${useCount === 1 ? "" : "s"} for the current session branch.`,
|
|
626
|
+
);
|
|
627
|
+
if (!confirmed) return;
|
|
628
|
+
await applyCommand("reset", ctx);
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
case "close":
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
|
|
431
636
|
function syncActiveTool(api: ExtensionAPI): void {
|
|
432
637
|
const activeTools = api.getActiveTools();
|
|
433
638
|
const hasAdvisor = activeTools.includes(TOOL_NAME);
|
|
@@ -460,6 +665,7 @@ export default function advisorPiExtension(pi: ExtensionAPI) {
|
|
|
460
665
|
`model: ${config.provider}/${config.modelId} (${availability})`,
|
|
461
666
|
`thinking: ${config.thinkingLevel}`,
|
|
462
667
|
`uses: ${useCount}/${config.maxUses}`,
|
|
668
|
+
`transcript: max ${config.maxTranscriptChars} chars`,
|
|
463
669
|
`cache: ${config.cacheRetention}`,
|
|
464
670
|
].join(" • ");
|
|
465
671
|
}
|
|
@@ -477,7 +683,7 @@ Your role is strategic guidance only. You do not have tools and you do not make
|
|
|
477
683
|
|
|
478
684
|
Return guidance in short sections with bullets when useful.`;
|
|
479
685
|
|
|
480
|
-
function defaultConfig(): AdvisorConfig {
|
|
686
|
+
export function defaultConfig(): AdvisorConfig {
|
|
481
687
|
const parsed = parseModelSpec(DEFAULT_ADVISOR_MODEL) ?? { provider: "openai-codex", modelId: "gpt-5.6-sol" };
|
|
482
688
|
return {
|
|
483
689
|
enabled: true,
|
|
@@ -488,6 +694,7 @@ function defaultConfig(): AdvisorConfig {
|
|
|
488
694
|
cacheRetention: DEFAULT_CACHE_RETENTION,
|
|
489
695
|
maxTokens: DEFAULT_MAX_TOKENS,
|
|
490
696
|
timeoutMs: DEFAULT_TIMEOUT_MS,
|
|
697
|
+
maxTranscriptChars: DEFAULT_MAX_TRANSCRIPT_CHARS,
|
|
491
698
|
};
|
|
492
699
|
}
|
|
493
700
|
|
|
@@ -509,6 +716,13 @@ function serializeCurrentConversation(ctx: ExtensionContext): string {
|
|
|
509
716
|
return serializeConversation(llmMessages);
|
|
510
717
|
}
|
|
511
718
|
|
|
719
|
+
export function truncateTranscript(text: string, maxChars: number): string {
|
|
720
|
+
if (maxChars <= 0) return text;
|
|
721
|
+
if (text.length <= maxChars) return text;
|
|
722
|
+
const omitted = text.length - maxChars;
|
|
723
|
+
return `[... earlier conversation truncated: showing the most recent ${maxChars} of ${text.length} characters (${omitted} omitted)]\n\n${text.slice(text.length - maxChars)}`;
|
|
724
|
+
}
|
|
725
|
+
|
|
512
726
|
function buildAdvisorUserPrompt(params: AdvisorToolInput, conversationText: string): string {
|
|
513
727
|
return [
|
|
514
728
|
"## Executor Question",
|
|
@@ -572,7 +786,7 @@ function makeSkippedDetails(config: AdvisorConfig, useCount: number, params: Adv
|
|
|
572
786
|
};
|
|
573
787
|
}
|
|
574
788
|
|
|
575
|
-
function makeStateEntry(config: AdvisorConfig, useCount: number): AdvisorStateEntry {
|
|
789
|
+
export function makeStateEntry(config: AdvisorConfig, useCount: number): AdvisorStateEntry {
|
|
576
790
|
return {
|
|
577
791
|
version: 1,
|
|
578
792
|
config: { ...config },
|
|
@@ -581,48 +795,116 @@ function makeStateEntry(config: AdvisorConfig, useCount: number): AdvisorStateEn
|
|
|
581
795
|
};
|
|
582
796
|
}
|
|
583
797
|
|
|
584
|
-
function persistState(pi: ExtensionAPI, config: AdvisorConfig, useCount: number): void {
|
|
798
|
+
export function persistState(pi: ExtensionAPI, config: AdvisorConfig, useCount: number): void {
|
|
585
799
|
pi.appendEntry(STATE_ENTRY, makeStateEntry(config, useCount));
|
|
586
800
|
}
|
|
587
801
|
|
|
588
|
-
function normalizeConfig(
|
|
802
|
+
export function normalizeConfig(
|
|
803
|
+
input: Partial<AdvisorConfig>,
|
|
804
|
+
fallback: AdvisorConfig,
|
|
805
|
+
registry?: ModelRegistryLike,
|
|
806
|
+
options: NormalizeConfigOptions = {},
|
|
807
|
+
): AdvisorConfig {
|
|
589
808
|
const normalized: AdvisorConfig = {
|
|
590
809
|
enabled: typeof input.enabled === "boolean" ? input.enabled : fallback.enabled,
|
|
591
|
-
provider: typeof input.provider === "string" && input.provider ? input.provider : fallback.provider,
|
|
592
|
-
modelId: typeof input.modelId === "string" && input.modelId ? input.modelId : fallback.modelId,
|
|
810
|
+
provider: typeof input.provider === "string" && input.provider.trim() ? input.provider.trim() : fallback.provider,
|
|
811
|
+
modelId: typeof input.modelId === "string" && input.modelId.trim() ? input.modelId.trim() : fallback.modelId,
|
|
593
812
|
thinkingLevel: parseThinkingLevel(input.thinkingLevel) ?? fallback.thinkingLevel,
|
|
594
|
-
maxUses:
|
|
813
|
+
maxUses:
|
|
814
|
+
typeof input.maxUses === "number" && Number.isFinite(input.maxUses) && Math.floor(input.maxUses) > 0
|
|
815
|
+
? Math.floor(input.maxUses)
|
|
816
|
+
: fallback.maxUses,
|
|
595
817
|
cacheRetention: parseCacheRetention(input.cacheRetention) ?? fallback.cacheRetention,
|
|
596
|
-
maxTokens:
|
|
597
|
-
|
|
818
|
+
maxTokens:
|
|
819
|
+
typeof input.maxTokens === "number" && Number.isFinite(input.maxTokens) && Math.floor(input.maxTokens) > 0
|
|
820
|
+
? Math.floor(input.maxTokens)
|
|
821
|
+
: fallback.maxTokens,
|
|
822
|
+
timeoutMs:
|
|
823
|
+
typeof input.timeoutMs === "number" && Number.isFinite(input.timeoutMs) && Math.floor(input.timeoutMs) > 0
|
|
824
|
+
? Math.floor(input.timeoutMs)
|
|
825
|
+
: fallback.timeoutMs,
|
|
826
|
+
maxTranscriptChars:
|
|
827
|
+
typeof input.maxTranscriptChars === "number" && Number.isFinite(input.maxTranscriptChars) && Math.floor(input.maxTranscriptChars) > 0
|
|
828
|
+
? Math.floor(input.maxTranscriptChars)
|
|
829
|
+
: fallback.maxTranscriptChars,
|
|
598
830
|
};
|
|
599
831
|
|
|
600
832
|
const legacyDefault = parseModelSpec(LEGACY_DEFAULT_ADVISOR_MODEL);
|
|
601
833
|
if (
|
|
834
|
+
options.migrateLegacyDefault !== false &&
|
|
602
835
|
input.thinkingLevel === undefined &&
|
|
603
836
|
legacyDefault &&
|
|
604
837
|
normalized.provider === legacyDefault.provider &&
|
|
605
838
|
normalized.modelId === legacyDefault.modelId
|
|
606
839
|
) {
|
|
607
|
-
|
|
608
|
-
|
|
840
|
+
// Only rewrite the stored legacy default when the migration target resolves.
|
|
841
|
+
// Otherwise a resolving stored model would be replaced by a non-resolving default.
|
|
842
|
+
if (!registry || modelResolves(registry, fallback.provider, fallback.modelId)) {
|
|
843
|
+
normalized.provider = fallback.provider;
|
|
844
|
+
normalized.modelId = fallback.modelId;
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
if (registry && typeof input.provider === "string" && input.provider && typeof input.modelId === "string" && input.modelId) {
|
|
849
|
+
// Never replace a resolving stored model with a non-resolving one.
|
|
850
|
+
if (modelResolves(registry, input.provider, input.modelId) && !modelResolves(registry, normalized.provider, normalized.modelId)) {
|
|
851
|
+
normalized.provider = input.provider;
|
|
852
|
+
normalized.modelId = input.modelId;
|
|
853
|
+
}
|
|
609
854
|
}
|
|
610
855
|
|
|
611
856
|
return normalized;
|
|
612
857
|
}
|
|
613
858
|
|
|
614
|
-
function
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
859
|
+
export function modelResolves(registry: ModelRegistryLike | undefined, provider: string, modelId: string): boolean {
|
|
860
|
+
if (!registry) return false;
|
|
861
|
+
try {
|
|
862
|
+
const found = registry.find(provider, modelId);
|
|
863
|
+
return found !== undefined && found !== null;
|
|
864
|
+
} catch {
|
|
865
|
+
return false;
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
export function resolveAdvisorModel(
|
|
870
|
+
registry: ModelRegistryLike | undefined,
|
|
871
|
+
candidates: Array<{ provider: string; modelId: string }>,
|
|
872
|
+
): { provider: string; modelId: string } | undefined {
|
|
873
|
+
if (!registry) return undefined;
|
|
874
|
+
for (const candidate of candidates) {
|
|
875
|
+
if (modelResolves(registry, candidate.provider, candidate.modelId)) return candidate;
|
|
876
|
+
}
|
|
877
|
+
try {
|
|
878
|
+
const available = registry.getAvailable?.();
|
|
879
|
+
const first = Array.isArray(available) ? available[0] : undefined;
|
|
880
|
+
if (first && typeof first.provider === "string" && typeof first.id === "string" && first.provider && first.id) {
|
|
881
|
+
return { provider: first.provider, modelId: first.id };
|
|
882
|
+
}
|
|
883
|
+
} catch {
|
|
884
|
+
// Ignore registry listing failures; callers keep the current config.
|
|
885
|
+
}
|
|
886
|
+
return undefined;
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
export function parseModelSpec(value: string): { provider: string; modelId: string } | undefined {
|
|
890
|
+
const trimmed = value.trim();
|
|
891
|
+
const slash = trimmed.indexOf("/");
|
|
892
|
+
if (slash <= 0 || slash === trimmed.length - 1) return undefined;
|
|
893
|
+
const provider = trimmed.slice(0, slash).trim();
|
|
894
|
+
const modelId = trimmed.slice(slash + 1).trim();
|
|
895
|
+
if (!provider || !modelId) return undefined;
|
|
896
|
+
return { provider, modelId };
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
function flagValue(api: ExtensionAPI, name: string): boolean | string | undefined {
|
|
900
|
+
return typeof api.getFlag === "function" ? api.getFlag(name) : undefined;
|
|
621
901
|
}
|
|
622
902
|
|
|
623
903
|
function parsePositiveInt(value: string): number | undefined {
|
|
624
|
-
const
|
|
625
|
-
if (
|
|
904
|
+
const trimmed = value.trim();
|
|
905
|
+
if (!/^\d+$/.test(trimmed)) return undefined;
|
|
906
|
+
const parsed = Number(trimmed);
|
|
907
|
+
if (!Number.isSafeInteger(parsed) || parsed <= 0) return undefined;
|
|
626
908
|
return parsed;
|
|
627
909
|
}
|
|
628
910
|
|