pi-advisor-flow 0.7.0 → 0.8.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/CHANGELOG.md +19 -18
- package/README.md +9 -8
- package/dist/index.js +1390 -1068
- package/extensions/index.ts +2 -1
- package/package.json +48 -35
- package/src/attachments.ts +11 -10
- package/src/commands/activation-preparation.ts +1 -0
- package/src/commands/activation.ts +2 -0
- package/src/commands/lifecycle.ts +1 -0
- package/src/commands/manual-command.ts +7 -1
- package/src/commands/manual-consultation.ts +102 -92
- package/src/commands/manual-progress.ts +7 -5
- package/src/commands/model-commands.ts +1 -1
- package/src/commands/model-options.ts +14 -3
- package/src/commands/model-picker.ts +1 -0
- package/src/commands/registration.ts +1 -0
- package/src/commands/renderers.ts +67 -55
- package/src/commands/runtime.ts +16 -14
- package/src/commands/settings-commands.ts +10 -4
- package/src/commands/settings-persistence.ts +16 -2
- package/src/commands/types.ts +1 -0
- package/src/commands.ts +0 -1
- package/src/config/args.ts +1 -1
- package/src/config/defaults.ts +8 -1
- package/src/config/schema.ts +42 -17
- package/src/config/state.ts +12 -3
- package/src/config/storage.ts +34 -23
- package/src/config/types.ts +16 -3
- package/src/config/validation.ts +33 -21
- package/src/config.ts +3 -0
- package/src/content-utils.ts +24 -4
- package/src/conversation.ts +16 -15
- package/src/git.ts +10 -3
- package/src/herdr-block.ts +83 -0
- package/src/herdr-shared.ts +108 -0
- package/src/herdr.ts +30 -192
- package/src/jev/client.ts +75 -85
- package/src/jev/failure.ts +19 -0
- package/src/jev/key-store.ts +26 -11
- package/src/jev/ledger.ts +58 -64
- package/src/jev/questions.ts +26 -23
- package/src/jev/state.ts +15 -12
- package/src/jev/transport.ts +8 -3
- package/src/model-stream.ts +35 -37
- package/src/outcomes.ts +15 -11
- package/src/pi-settings.ts +4 -9
- package/src/preferences.ts +4 -2
- package/src/redaction.ts +10 -12
- package/src/scout-context.ts +59 -60
- package/src/scout-curation.ts +4 -13
- package/src/scout-groups.ts +178 -175
- package/src/scout-protocol.ts +74 -70
- package/src/scout-reconstruct.ts +1 -1
- package/src/scout.ts +115 -73
- package/src/session-state.ts +64 -49
- package/src/tool-result-cap.ts +18 -17
- package/src/tools/consult-context.ts +7 -15
- package/src/tools/consultation.ts +14 -4
- package/src/tools/gate-policy.ts +1 -0
- package/src/tools/gate-protocol.ts +36 -23
- package/src/tools/jev-filter.ts +43 -41
- package/src/tools/jev-turn-gate.ts +29 -9
- package/src/tools/loop-gate.ts +31 -20
- package/src/tools/model-access.ts +46 -0
- package/src/tools/prompts.ts +20 -25
- package/src/tools/register-ask-advisor.ts +36 -16
- package/src/tools/register-lifecycle.ts +22 -0
- package/src/tools/register-outcome.ts +2 -0
- package/src/tools/register-renderers.ts +146 -128
- package/src/tools/registration.ts +12 -14
- package/src/tools/render-advisor-result.ts +35 -22
- package/src/tools/render-common.ts +16 -19
- package/src/tools/scout-status.ts +42 -27
- package/src/tools/types.ts +1 -0
- package/src/tools.ts +0 -1
- package/src/ui/jev-setup-submenu.ts +65 -59
- package/src/ui/manual-dialog-render.ts +1 -0
- package/src/ui/manual-dialog.ts +39 -43
- package/src/ui/masked-input.ts +5 -11
- package/src/ui/model-multi-selector.ts +9 -0
- package/src/ui/model-selector-adapter.ts +31 -0
- package/src/ui/model-selector.ts +14 -161
- package/src/ui/searchable-model-list.ts +230 -0
- package/src/ui/settings-formatting.ts +24 -14
- package/src/ui/settings-items.ts +38 -1
- package/src/ui/settings-list-adapter.ts +15 -12
- package/src/ui/settings-mutations.ts +157 -70
- package/src/ui/settings-selector.ts +18 -9
- package/src/ui/text-setting-submenu.ts +3 -6
- package/src/ui/types.ts +14 -0
- package/src/ui.ts +0 -1
- package/src/usage.ts +26 -19
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { getMarkdownTheme } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type {
|
|
3
|
+
EntryRenderer,
|
|
4
|
+
MessageRenderer,
|
|
5
|
+
} from "@earendil-works/pi-coding-agent";
|
|
2
6
|
import { Box, Markdown, Text } from "@earendil-works/pi-tui";
|
|
7
|
+
|
|
3
8
|
import { getAdvisorSettings } from "../config/state.ts";
|
|
9
|
+
import { isString } from "../content-utils.ts";
|
|
4
10
|
import {
|
|
5
11
|
adviceForDisplay,
|
|
6
12
|
hasSoundVerdict,
|
|
@@ -11,66 +17,72 @@ import { formatAdvisorUsage } from "../usage.ts";
|
|
|
11
17
|
import { ManualAdvisorProgressComponent } from "./manual-progress.ts";
|
|
12
18
|
import type { CommandRuntime } from "./types.ts";
|
|
13
19
|
|
|
20
|
+
interface ManualCallEntryDetails {
|
|
21
|
+
progressId?: string;
|
|
22
|
+
question?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface ManualResultDetails {
|
|
26
|
+
advisor?: string;
|
|
27
|
+
text?: string;
|
|
28
|
+
usage?: unknown;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const manualCallRenderer =
|
|
32
|
+
(runtime: CommandRuntime): EntryRenderer<ManualCallEntryDetails> =>
|
|
33
|
+
(entry, { expanded }, theme) => {
|
|
34
|
+
const { progressId, question } = entry.data ?? {};
|
|
35
|
+
const progress = progressId
|
|
36
|
+
? runtime.manualProgress.get(progressId)
|
|
37
|
+
: undefined;
|
|
38
|
+
return progress
|
|
39
|
+
? new ManualAdvisorProgressComponent(
|
|
40
|
+
question,
|
|
41
|
+
progress,
|
|
42
|
+
Boolean(expanded),
|
|
43
|
+
theme
|
|
44
|
+
)
|
|
45
|
+
: renderAdvisorCallBox(question, theme);
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const manualResultRenderer: MessageRenderer<ManualResultDetails> = (
|
|
49
|
+
message,
|
|
50
|
+
{ expanded },
|
|
51
|
+
theme
|
|
52
|
+
) => {
|
|
53
|
+
const { details } = message;
|
|
54
|
+
const box = new Box(1, 1, (text) => theme.bg("customMessageBg", text));
|
|
55
|
+
const advice =
|
|
56
|
+
details?.text ??
|
|
57
|
+
(isString(message.content)
|
|
58
|
+
? message.content
|
|
59
|
+
: "(Advisor returned no advice.)");
|
|
60
|
+
// Manual consultations must render exactly like an Executor ask_advisor call.
|
|
61
|
+
box.addChild(
|
|
62
|
+
new Text(renderAdvisorResponseHeader(hasSoundVerdict(advice), theme), 0, 0)
|
|
63
|
+
);
|
|
64
|
+
if (details?.advisor) {
|
|
65
|
+
box.addChild(new Text(theme.fg("dim", ` ${details.advisor}`), 0, 0));
|
|
66
|
+
}
|
|
67
|
+
if (getAdvisorSettings().showUsageDetails) {
|
|
68
|
+
const usage = formatAdvisorUsage(details?.usage);
|
|
69
|
+
if (usage) {
|
|
70
|
+
box.addChild(new Text(theme.fg("dim", ` Usage: ${usage}`), 0, 0));
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
box.addChild(
|
|
74
|
+
new Markdown(adviceForDisplay(advice, expanded), 0, 0, getMarkdownTheme())
|
|
75
|
+
);
|
|
76
|
+
return box;
|
|
77
|
+
};
|
|
78
|
+
|
|
14
79
|
export const registerCommandRenderers = (runtime: CommandRuntime) => {
|
|
15
80
|
runtime.pi.registerEntryRenderer?.(
|
|
16
81
|
"advisor-manual-call",
|
|
17
|
-
(
|
|
18
|
-
const { progressId, question } = (entry.data ?? {}) as {
|
|
19
|
-
progressId?: string;
|
|
20
|
-
question?: string;
|
|
21
|
-
};
|
|
22
|
-
const progress = progressId
|
|
23
|
-
? runtime.manualProgress.get(progressId)
|
|
24
|
-
: undefined;
|
|
25
|
-
return progress
|
|
26
|
-
? new ManualAdvisorProgressComponent(
|
|
27
|
-
question,
|
|
28
|
-
progress,
|
|
29
|
-
Boolean(expanded),
|
|
30
|
-
theme
|
|
31
|
-
)
|
|
32
|
-
: renderAdvisorCallBox(question, theme);
|
|
33
|
-
}
|
|
82
|
+
manualCallRenderer(runtime)
|
|
34
83
|
);
|
|
35
|
-
|
|
36
84
|
runtime.pi.registerMessageRenderer?.(
|
|
37
85
|
"advisor-manual-result",
|
|
38
|
-
|
|
39
|
-
const details = message.details as
|
|
40
|
-
| { advisor?: string; text?: string; usage?: unknown }
|
|
41
|
-
| undefined;
|
|
42
|
-
const box = new Box(1, 1, (text) => theme.bg("customMessageBg", text));
|
|
43
|
-
const advice =
|
|
44
|
-
details?.text ??
|
|
45
|
-
(typeof message.content === "string"
|
|
46
|
-
? message.content
|
|
47
|
-
: "(Advisor returned no advice.)");
|
|
48
|
-
// Manual consultations must render exactly like an Executor ask_advisor call.
|
|
49
|
-
box.addChild(
|
|
50
|
-
new Text(
|
|
51
|
-
renderAdvisorResponseHeader(hasSoundVerdict(advice), theme),
|
|
52
|
-
0,
|
|
53
|
-
0
|
|
54
|
-
)
|
|
55
|
-
);
|
|
56
|
-
if (details?.advisor) {
|
|
57
|
-
box.addChild(new Text(theme.fg("dim", ` ${details.advisor}`), 0, 0));
|
|
58
|
-
}
|
|
59
|
-
if (getAdvisorSettings().showUsageDetails) {
|
|
60
|
-
const usage = formatAdvisorUsage(details?.usage);
|
|
61
|
-
if (usage) {
|
|
62
|
-
box.addChild(new Text(theme.fg("dim", ` Usage: ${usage}`), 0, 0));
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
box.addChild(
|
|
66
|
-
new Markdown(
|
|
67
|
-
adviceForDisplay(advice, expanded),
|
|
68
|
-
0,
|
|
69
|
-
0,
|
|
70
|
-
getMarkdownTheme()
|
|
71
|
-
)
|
|
72
|
-
);
|
|
73
|
-
return box;
|
|
74
|
-
}
|
|
86
|
+
manualResultRenderer
|
|
75
87
|
);
|
|
76
88
|
};
|
package/src/commands/runtime.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type {
|
|
|
2
2
|
ExtensionAPI,
|
|
3
3
|
ExtensionContext,
|
|
4
4
|
} from "@earendil-works/pi-coding-agent";
|
|
5
|
+
|
|
5
6
|
import { getAdvisorSettings } from "../config/state.ts";
|
|
6
7
|
import { notifyHerdrAdvisorFailure } from "../herdr.ts";
|
|
7
8
|
import type { AdvisorSessionState } from "../session-state.ts";
|
|
@@ -25,6 +26,18 @@ export const notify = (
|
|
|
25
26
|
}
|
|
26
27
|
};
|
|
27
28
|
|
|
29
|
+
const reportManualBudgetExhausted = (ctx: ExtensionContext) => {
|
|
30
|
+
const message = "Advisor call budget exhausted for this session.";
|
|
31
|
+
notify(ctx, message, "warning");
|
|
32
|
+
notifyHerdrAdvisorFailure("Advisor budget exhausted", message);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const requestManualRender = (ctx: ExtensionContext) => {
|
|
36
|
+
if (ctx.hasUI) {
|
|
37
|
+
ctx.ui.setStatus("advisor-manual", undefined);
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
28
41
|
class CommandRuntime implements CommandRuntimeContract {
|
|
29
42
|
readonly advisorSessionState: AdvisorSessionState;
|
|
30
43
|
readonly manualConsultations = new Map<AbortController, symbol>();
|
|
@@ -34,7 +47,9 @@ class CommandRuntime implements CommandRuntimeContract {
|
|
|
34
47
|
ReturnType<typeof setInterval>
|
|
35
48
|
>();
|
|
36
49
|
readonly pi: ExtensionAPI;
|
|
50
|
+
readonly reportManualBudgetExhausted = reportManualBudgetExhausted;
|
|
37
51
|
readonly requestAdvisor: ManualConsult;
|
|
52
|
+
readonly requestManualRender = requestManualRender;
|
|
38
53
|
readonly scoutStatus: ScoutStatusManager;
|
|
39
54
|
manualProgressSequence = 0;
|
|
40
55
|
pendingExecutorModelRef: string | undefined;
|
|
@@ -59,8 +74,7 @@ class CommandRuntime implements CommandRuntimeContract {
|
|
|
59
74
|
undefined,
|
|
60
75
|
undefined,
|
|
61
76
|
undefined,
|
|
62
|
-
onScout
|
|
63
|
-
undefined
|
|
77
|
+
onScout
|
|
64
78
|
));
|
|
65
79
|
}
|
|
66
80
|
|
|
@@ -94,18 +108,6 @@ class CommandRuntime implements CommandRuntimeContract {
|
|
|
94
108
|
);
|
|
95
109
|
}
|
|
96
110
|
}
|
|
97
|
-
|
|
98
|
-
reportManualBudgetExhausted(ctx: ExtensionContext) {
|
|
99
|
-
const message = "Advisor call budget exhausted for this session.";
|
|
100
|
-
notify(ctx, message, "warning");
|
|
101
|
-
notifyHerdrAdvisorFailure("Advisor budget exhausted", message);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
requestManualRender(ctx: ExtensionContext) {
|
|
105
|
-
if (ctx.hasUI) {
|
|
106
|
-
ctx.ui.setStatus("advisor-manual", undefined);
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
111
|
}
|
|
110
112
|
|
|
111
113
|
export const createCommandRuntime = (
|
|
@@ -7,7 +7,11 @@ import {
|
|
|
7
7
|
import { saveConfig } from "../config/storage.ts";
|
|
8
8
|
import { AdvisorSettingsSelector } from "../ui/settings-selector.ts";
|
|
9
9
|
import { loadCommandConfig } from "./activation-preparation.ts";
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
CONTEXT_PRESETS,
|
|
12
|
+
EFFORT_LEVELS,
|
|
13
|
+
getConfiguredModelRefs,
|
|
14
|
+
} from "./model-options.ts";
|
|
11
15
|
import { notify } from "./runtime.ts";
|
|
12
16
|
import { saveAdvisorSettings } from "./settings-persistence.ts";
|
|
13
17
|
import type { CommandRuntime } from "./types.ts";
|
|
@@ -21,12 +25,14 @@ export const registerSettingsCommands = (runtime: CommandRuntime) => {
|
|
|
21
25
|
}
|
|
22
26
|
|
|
23
27
|
const initial = getAdvisorSettings();
|
|
24
|
-
await ctx.ui.custom<
|
|
25
|
-
(tui, theme,
|
|
28
|
+
await ctx.ui.custom<undefined>(
|
|
29
|
+
(tui, theme, keybindings, done) =>
|
|
26
30
|
new AdvisorSettingsSelector({
|
|
27
31
|
effortLevels: EFFORT_LEVELS,
|
|
28
32
|
initial,
|
|
29
|
-
|
|
33
|
+
keybindings,
|
|
34
|
+
modelRefs: getConfiguredModelRefs(ctx),
|
|
35
|
+
onCancel: () => done(undefined),
|
|
30
36
|
onChange: (settings) => {
|
|
31
37
|
try {
|
|
32
38
|
saveAdvisorSettings(ctx, settings);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
|
|
2
3
|
import {
|
|
3
4
|
getPersistedModelRefs,
|
|
4
5
|
setAdvisorAutoLoopGateRef,
|
|
@@ -25,6 +26,7 @@ import {
|
|
|
25
26
|
setAdvisorJevTurnGateNoulThresholdRef,
|
|
26
27
|
setAdvisorLoopThresholdRef,
|
|
27
28
|
setAdvisorMaxCallsPerSessionRef,
|
|
29
|
+
setAdvisorModelWhitelistRef,
|
|
28
30
|
setAdvisorOutcomeLoggingRef,
|
|
29
31
|
setAdvisorPlanGateRef,
|
|
30
32
|
setAdvisorRedactSecretsRef,
|
|
@@ -56,8 +58,7 @@ import {
|
|
|
56
58
|
} from "../config/types.ts";
|
|
57
59
|
import type { AdvisorSettings } from "../ui/types.ts";
|
|
58
60
|
|
|
59
|
-
|
|
60
|
-
const applyAdvisorSettings = (settings: AdvisorSettings) => {
|
|
61
|
+
const applySessionSettings = (settings: AdvisorSettings) => {
|
|
61
62
|
setAdvisorEffortRef(
|
|
62
63
|
settings.effort === "Default (Model Default)" ? undefined : settings.effort
|
|
63
64
|
);
|
|
@@ -71,6 +72,7 @@ const applyAdvisorSettings = (settings: AdvisorSettings) => {
|
|
|
71
72
|
setAdvisorAutoLoopGateRef(settings.autoLoopGate ?? true);
|
|
72
73
|
setAdvisorLoopThresholdRef(settings.loopThreshold ?? 3);
|
|
73
74
|
setAdvisorMaxCallsPerSessionRef(settings.maxCallsPerSession);
|
|
75
|
+
setAdvisorModelWhitelistRef(settings.modelWhitelist ?? []);
|
|
74
76
|
setAdvisorSessionSummaryRef(settings.sessionSummary ?? false);
|
|
75
77
|
setAdvisorScoutEnabledRef(settings.scoutEnabled ?? false);
|
|
76
78
|
setShowUsageDetailsRef(settings.showUsageDetails ?? true);
|
|
@@ -79,6 +81,9 @@ const applyAdvisorSettings = (settings: AdvisorSettings) => {
|
|
|
79
81
|
setAlwaysOnRef(settings.alwaysOn ?? false);
|
|
80
82
|
setAdvisorFailureModeRef(settings.failureMode ?? "block-session");
|
|
81
83
|
setAdvisorHerdrIntegrationRef(settings.herdrIntegration ?? true);
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const applyJevSettings = (settings: AdvisorSettings) => {
|
|
82
87
|
setAdvisorJevFilterEnabledRef(settings.jevFilterEnabled ?? false);
|
|
83
88
|
setAdvisorJevFilterSkipConfidenceRef(
|
|
84
89
|
settings.jevFilterSkipConfidence ?? DEFAULT_JEV_FILTER_SKIP_CONFIDENCE
|
|
@@ -104,6 +109,9 @@ const applyAdvisorSettings = (settings: AdvisorSettings) => {
|
|
|
104
109
|
setAdvisorJevTurnGateNoulThresholdRef(
|
|
105
110
|
settings.jevTurnGateNoulThreshold ?? DEFAULT_JEV_TURN_GATE_NOUL_THRESHOLD
|
|
106
111
|
);
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const applyDisclosureSettings = (settings: AdvisorSettings) => {
|
|
107
115
|
setAdvisorToolResultMaxLinesRef(settings.toolResultMaxLines ?? 2000);
|
|
108
116
|
setAdvisorToolResultMaxBytesRef(settings.toolResultMaxBytes ?? 50 * 1024);
|
|
109
117
|
setAdvisorRedactSecretsRef(settings.redactSecrets ?? false);
|
|
@@ -115,6 +123,12 @@ const applyAdvisorSettings = (settings: AdvisorSettings) => {
|
|
|
115
123
|
setAdvisorOutcomeLoggingRef(settings.outcomeLogging ?? false);
|
|
116
124
|
};
|
|
117
125
|
|
|
126
|
+
const applyAdvisorSettings = (settings: AdvisorSettings) => {
|
|
127
|
+
applySessionSettings(settings);
|
|
128
|
+
applyJevSettings(settings);
|
|
129
|
+
applyDisclosureSettings(settings);
|
|
130
|
+
};
|
|
131
|
+
|
|
118
132
|
export const saveAdvisorSettings = (
|
|
119
133
|
ctx: ExtensionContext,
|
|
120
134
|
settings: AdvisorSettings
|
package/src/commands/types.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type {
|
|
|
2
2
|
ExtensionAPI,
|
|
3
3
|
ExtensionContext,
|
|
4
4
|
} from "@earendil-works/pi-coding-agent";
|
|
5
|
+
|
|
5
6
|
import type { GitContextLevel } from "../git.ts";
|
|
6
7
|
import type { ScoutLifecycleEvent } from "../scout.ts";
|
|
7
8
|
import type { AdvisorSessionState } from "../session-state.ts";
|
package/src/commands.ts
CHANGED
package/src/config/args.ts
CHANGED
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
import { MAX_CONTEXT_MAX_CHARS } from "./types.ts";
|
|
10
10
|
import { isValidContextMaxChars } from "./validation.ts";
|
|
11
11
|
|
|
12
|
-
const ARGUMENT_WHITESPACE = /\s
|
|
12
|
+
const ARGUMENT_WHITESPACE = /\s+/u;
|
|
13
13
|
|
|
14
14
|
export const parseArgs = (args: string): string | undefined => {
|
|
15
15
|
let nextExecutor = executorRef;
|
package/src/config/defaults.ts
CHANGED
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
setAdvisorJevTurnGateNoulThresholdRef,
|
|
24
24
|
setAdvisorLoopThresholdRef,
|
|
25
25
|
setAdvisorMaxCallsPerSessionRef,
|
|
26
|
+
setAdvisorModelWhitelistRef,
|
|
26
27
|
setAdvisorOutcomeLoggingRef,
|
|
27
28
|
setAdvisorPlanGateRef,
|
|
28
29
|
setAdvisorRedactSecretsRef,
|
|
@@ -77,6 +78,7 @@ export const resetDefaults = () => {
|
|
|
77
78
|
setAdvisorAutoLoopGateRef(true);
|
|
78
79
|
setAdvisorLoopThresholdRef(3);
|
|
79
80
|
setAdvisorMaxCallsPerSessionRef(undefined);
|
|
81
|
+
setAdvisorModelWhitelistRef([]);
|
|
80
82
|
setAdvisorSessionSummaryRef(false);
|
|
81
83
|
setSimpleModeRef(false);
|
|
82
84
|
setAlwaysOnRef(false);
|
|
@@ -114,7 +116,7 @@ const applyOptionalConfig = <Key extends keyof AdvisorConfig>(
|
|
|
114
116
|
) => {
|
|
115
117
|
const value = config[key];
|
|
116
118
|
if (value !== undefined) {
|
|
117
|
-
apply(value
|
|
119
|
+
apply(value);
|
|
118
120
|
}
|
|
119
121
|
};
|
|
120
122
|
|
|
@@ -166,6 +168,11 @@ export const applyConfig = (config: AdvisorConfig) => {
|
|
|
166
168
|
"advisorMaxCallsPerSession",
|
|
167
169
|
setAdvisorMaxCallsPerSessionRef
|
|
168
170
|
);
|
|
171
|
+
applyOptionalConfig(
|
|
172
|
+
config,
|
|
173
|
+
"advisorModelWhitelist",
|
|
174
|
+
setAdvisorModelWhitelistRef
|
|
175
|
+
);
|
|
169
176
|
applyOptionalConfig(
|
|
170
177
|
config,
|
|
171
178
|
"advisorSessionSummary",
|
package/src/config/schema.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { isString } from "../content-utils.ts";
|
|
2
|
+
import type { JsonValue } from "../content-utils.ts";
|
|
1
3
|
import { GIT_CONTEXT_LEVELS, isValidGitContextLevel } from "../git.ts";
|
|
2
4
|
import {
|
|
3
5
|
advisorAutoLoopGateRef,
|
|
@@ -24,6 +26,7 @@ import {
|
|
|
24
26
|
advisorJevTurnGateNoulThresholdRef,
|
|
25
27
|
advisorLoopThresholdRef,
|
|
26
28
|
advisorMaxCallsPerSessionRef,
|
|
29
|
+
advisorModelWhitelistRef,
|
|
27
30
|
advisorOutcomeLoggingRef,
|
|
28
31
|
advisorPlanGateRef,
|
|
29
32
|
advisorRedactSecretsRef,
|
|
@@ -43,12 +46,15 @@ import {
|
|
|
43
46
|
showUsageFooterRef,
|
|
44
47
|
simpleModeRef,
|
|
45
48
|
} from "./state.ts";
|
|
49
|
+
import type {
|
|
50
|
+
AdvisorConfig,
|
|
51
|
+
AdvisorConfigValue,
|
|
52
|
+
JevTransport,
|
|
53
|
+
} from "./types.ts";
|
|
46
54
|
import {
|
|
47
55
|
ADVISOR_TOOL_POLICIES,
|
|
48
|
-
type AdvisorConfig,
|
|
49
56
|
GATE_FAILURE_MODES,
|
|
50
57
|
JEV_TRANSPORTS,
|
|
51
|
-
type JevTransport,
|
|
52
58
|
MAX_CONTEXT_MAX_CHARS,
|
|
53
59
|
} from "./types.ts";
|
|
54
60
|
|
|
@@ -56,6 +62,15 @@ import {
|
|
|
56
62
|
const configuredModelRef = (value: string | undefined): string | undefined =>
|
|
57
63
|
value?.trim() || undefined;
|
|
58
64
|
|
|
65
|
+
const isNonEmptyModel = (model: unknown): model is string =>
|
|
66
|
+
typeof model === "string" && model.trim().length > 0;
|
|
67
|
+
|
|
68
|
+
export const isValidAdvisorModelWhitelist = (
|
|
69
|
+
value: unknown
|
|
70
|
+
): value is string[] => Array.isArray(value) && value.every(isNonEmptyModel);
|
|
71
|
+
|
|
72
|
+
const ADVISOR_TOOL_POLICY_NAMES = new Set<string>(ADVISOR_TOOL_POLICIES);
|
|
73
|
+
|
|
59
74
|
export const isValidAdvisorToolPolicies = (
|
|
60
75
|
value: unknown
|
|
61
76
|
): value is Record<string, (typeof ADVISOR_TOOL_POLICIES)[number]> => {
|
|
@@ -65,10 +80,8 @@ export const isValidAdvisorToolPolicies = (
|
|
|
65
80
|
return Object.entries(value).every(
|
|
66
81
|
([toolName, policy]) =>
|
|
67
82
|
toolName.trim().length > 0 &&
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
policy as (typeof ADVISOR_TOOL_POLICIES)[number]
|
|
71
|
-
)
|
|
83
|
+
isString(policy) &&
|
|
84
|
+
ADVISOR_TOOL_POLICY_NAMES.has(policy)
|
|
72
85
|
);
|
|
73
86
|
};
|
|
74
87
|
|
|
@@ -87,11 +100,12 @@ export const isValidLoopThreshold = (value: unknown): value is number =>
|
|
|
87
100
|
export const isValidMaxCallsPerSession = (value: unknown): value is number =>
|
|
88
101
|
nonNegativeSafeInteger(value);
|
|
89
102
|
|
|
103
|
+
const GATE_FAILURE_MODE_NAMES = new Set<string>(GATE_FAILURE_MODES);
|
|
104
|
+
|
|
90
105
|
export const isValidGateFailureMode = (
|
|
91
106
|
value: unknown
|
|
92
107
|
): value is (typeof GATE_FAILURE_MODES)[number] =>
|
|
93
|
-
|
|
94
|
-
GATE_FAILURE_MODES.includes(value as (typeof GATE_FAILURE_MODES)[number]);
|
|
108
|
+
isString(value) && GATE_FAILURE_MODE_NAMES.has(value);
|
|
95
109
|
|
|
96
110
|
export const isValidToolResultMaxLines = (value: unknown): value is number =>
|
|
97
111
|
nonNegativeSafeInteger(value);
|
|
@@ -120,8 +134,10 @@ export const isValidJevNoulMargin = (value: unknown): value is number =>
|
|
|
120
134
|
value >= 0 &&
|
|
121
135
|
value <= 0.5;
|
|
122
136
|
|
|
137
|
+
const JEV_TRANSPORT_NAMES = new Set<string>(JEV_TRANSPORTS);
|
|
138
|
+
|
|
123
139
|
export const isValidJevTransport = (value: unknown): value is JevTransport =>
|
|
124
|
-
|
|
140
|
+
isString(value) && JEV_TRANSPORT_NAMES.has(value);
|
|
125
141
|
|
|
126
142
|
/** One declarative entry per AdvisorConfig key: JSON type, persistence, and
|
|
127
143
|
* the live runtime value behind it. Validation and storage derive from this
|
|
@@ -130,13 +146,13 @@ export interface ConfigKeySchema {
|
|
|
130
146
|
/** Human-readable accepted-value phrase for error messages. */
|
|
131
147
|
accepted: string;
|
|
132
148
|
/** Live runtime value captured by saveConfig. */
|
|
133
|
-
current: () =>
|
|
149
|
+
current: () => AdvisorConfigValue;
|
|
134
150
|
/** Included in the state captured and diffed by saveConfig. */
|
|
135
151
|
persisted: boolean;
|
|
136
152
|
/** JSON value type used for the base type check. */
|
|
137
|
-
type: "string" | "boolean" | "number" | "enum" | "object";
|
|
153
|
+
type: "string" | "boolean" | "number" | "enum" | "object" | "array";
|
|
138
154
|
/** Type beyond the JSON type, for enum and object keys. */
|
|
139
|
-
validate?: (value: unknown) =>
|
|
155
|
+
validate?: (value: unknown) => value is JsonValue;
|
|
140
156
|
}
|
|
141
157
|
|
|
142
158
|
export const CONFIG_SCHEMA = {
|
|
@@ -297,6 +313,13 @@ export const CONFIG_SCHEMA = {
|
|
|
297
313
|
type: "number",
|
|
298
314
|
validate: isValidMaxCallsPerSession,
|
|
299
315
|
},
|
|
316
|
+
advisorModelWhitelist: {
|
|
317
|
+
accepted: "an array of non-empty provider/model strings",
|
|
318
|
+
current: () => [...advisorModelWhitelistRef],
|
|
319
|
+
persisted: true,
|
|
320
|
+
type: "array",
|
|
321
|
+
validate: isValidAdvisorModelWhitelist,
|
|
322
|
+
},
|
|
300
323
|
advisorOutcomeLogging: {
|
|
301
324
|
accepted: "true or false",
|
|
302
325
|
current: () => advisorOutcomeLoggingRef,
|
|
@@ -415,6 +438,9 @@ export const CONFIG_SCHEMA = {
|
|
|
415
438
|
|
|
416
439
|
export type ConfigKey = keyof typeof CONFIG_SCHEMA;
|
|
417
440
|
|
|
441
|
+
// SAFETY: CONFIG_SCHEMA satisfies Record<keyof AdvisorConfig, ConfigKeySchema>, so Object.keys yields exactly ConfigKey.
|
|
442
|
+
export const configKeys = Object.keys(CONFIG_SCHEMA) as ConfigKey[];
|
|
443
|
+
|
|
418
444
|
/** Keys saveConfig captures, diffs, and writes to advisor.json. */
|
|
419
445
|
export type PersistedConfigKey = {
|
|
420
446
|
[Key in ConfigKey]: (typeof CONFIG_SCHEMA)[Key]["persisted"] extends true
|
|
@@ -422,11 +448,10 @@ export type PersistedConfigKey = {
|
|
|
422
448
|
: never;
|
|
423
449
|
}[ConfigKey];
|
|
424
450
|
|
|
425
|
-
export const SAVED_CONFIG_KEYS =
|
|
426
|
-
|
|
427
|
-
).
|
|
428
|
-
|
|
429
|
-
) as readonly PersistedConfigKey[];
|
|
451
|
+
export const SAVED_CONFIG_KEYS: readonly PersistedConfigKey[] =
|
|
452
|
+
configKeys.filter(
|
|
453
|
+
(key): key is PersistedConfigKey => CONFIG_SCHEMA[key].persisted
|
|
454
|
+
);
|
|
430
455
|
|
|
431
456
|
/** Widened per-key view for consumers that index by dynamic key. */
|
|
432
457
|
const SCHEMA_BY_KEY: Record<ConfigKey, ConfigKeySchema> = CONFIG_SCHEMA;
|
package/src/config/state.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type { GitContextLevel } from "../git.ts";
|
|
2
2
|
import {
|
|
3
|
-
type AdvisorToolPolicies,
|
|
4
3
|
DEFAULT_ADVISOR_GIT_CONTEXT_MAX_CHARS,
|
|
5
4
|
DEFAULT_ADVISOR_TOOL_RESULT_MAX_BYTES,
|
|
6
5
|
DEFAULT_ADVISOR_TOOL_RESULT_MAX_LINES,
|
|
@@ -15,8 +14,11 @@ import {
|
|
|
15
14
|
DEFAULT_JEV_TRANSPORT,
|
|
16
15
|
DEFAULT_JEV_TURN_GATE_EVERY_TURNS,
|
|
17
16
|
DEFAULT_JEV_TURN_GATE_NOUL_THRESHOLD,
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
} from "./types.ts";
|
|
18
|
+
import type {
|
|
19
|
+
AdvisorToolPolicies,
|
|
20
|
+
GateFailureMode,
|
|
21
|
+
JevTransport,
|
|
20
22
|
} from "./types.ts";
|
|
21
23
|
|
|
22
24
|
// An empty ref means no model has been selected yet.
|
|
@@ -36,6 +38,7 @@ export let advisorBlockOnBlockedRef = true;
|
|
|
36
38
|
export let advisorAutoLoopGateRef = true;
|
|
37
39
|
export let advisorLoopThresholdRef = 3;
|
|
38
40
|
export let advisorMaxCallsPerSessionRef: number | undefined;
|
|
41
|
+
export let advisorModelWhitelistRef: string[] = [];
|
|
39
42
|
export let advisorSessionSummaryRef = false;
|
|
40
43
|
export let simpleModeRef = false;
|
|
41
44
|
export let alwaysOnRef = false;
|
|
@@ -125,6 +128,11 @@ export const setAdvisorLoopThresholdRef = (value: number) => {
|
|
|
125
128
|
export const setAdvisorMaxCallsPerSessionRef = (value: number | undefined) => {
|
|
126
129
|
advisorMaxCallsPerSessionRef = value;
|
|
127
130
|
};
|
|
131
|
+
export const setAdvisorModelWhitelistRef = (models: string[]) => {
|
|
132
|
+
advisorModelWhitelistRef = [
|
|
133
|
+
...new Set(models.map((model) => model.trim()).filter(Boolean)),
|
|
134
|
+
];
|
|
135
|
+
};
|
|
128
136
|
export const setAdvisorSessionSummaryRef = (enabled: boolean) => {
|
|
129
137
|
advisorSessionSummaryRef = enabled;
|
|
130
138
|
};
|
|
@@ -243,6 +251,7 @@ export const getAdvisorSettings = () => ({
|
|
|
243
251
|
jevTurnGateNoulThreshold: advisorJevTurnGateNoulThresholdRef,
|
|
244
252
|
loopThreshold: advisorLoopThresholdRef,
|
|
245
253
|
maxCallsPerSession: advisorMaxCallsPerSessionRef,
|
|
254
|
+
modelWhitelist: [...advisorModelWhitelistRef],
|
|
246
255
|
outcomeLogging: advisorOutcomeLoggingRef,
|
|
247
256
|
planGate: advisorPlanGateRef,
|
|
248
257
|
redactSecrets: advisorRedactSecretsRef,
|
package/src/config/storage.ts
CHANGED
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
import { existsSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
} from "
|
|
3
|
+
|
|
4
|
+
import { CONFIG_DIR_NAME, getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
5
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
6
|
+
|
|
7
|
+
import { isRecord, isString } from "../content-utils.ts";
|
|
8
|
+
import type { RecordValue } from "../content-utils.ts";
|
|
8
9
|
import { applyConfig, resetDefaults } from "./defaults.ts";
|
|
9
10
|
import {
|
|
10
11
|
CONFIG_SCHEMA,
|
|
11
12
|
configuredModelRef,
|
|
12
|
-
type PersistedConfigKey,
|
|
13
13
|
SAVED_CONFIG_KEYS,
|
|
14
14
|
} from "./schema.ts";
|
|
15
|
+
import type { PersistedConfigKey } from "./schema.ts";
|
|
15
16
|
import { setAdvisorOutcomeLoggingRef, setPersistedModelRefs } from "./state.ts";
|
|
16
17
|
import type { AdvisorConfig } from "./types.ts";
|
|
17
18
|
import { unknownConfigKeys, validateConfig } from "./validation.ts";
|
|
@@ -29,6 +30,7 @@ type ConfigState = {
|
|
|
29
30
|
};
|
|
30
31
|
|
|
31
32
|
const currentConfigState = (): ConfigState =>
|
|
33
|
+
// SAFETY: SAVED_CONFIG_KEYS enumerates exactly the ConfigState keys, so fromEntries yields ConfigState.
|
|
32
34
|
Object.fromEntries(
|
|
33
35
|
SAVED_CONFIG_KEYS.map((key) => [key, CONFIG_SCHEMA[key].current()])
|
|
34
36
|
) as ConfigState;
|
|
@@ -41,12 +43,10 @@ const sameConfigValue = <Value>(left: Value, right: Value) =>
|
|
|
41
43
|
* preserved like any unknown key, and never trigger the typo warning. */
|
|
42
44
|
const RESERVED_ADVISOR_JSON_KEYS = new Set(["typesafe_api_key"]);
|
|
43
45
|
|
|
44
|
-
export const readExistingConfig = (path: string):
|
|
46
|
+
export const readExistingConfig = (path: string): RecordValue => {
|
|
45
47
|
try {
|
|
46
|
-
const parsed = JSON.parse(readFileSync(path, "
|
|
47
|
-
return parsed
|
|
48
|
-
? (parsed as Record<string, unknown>)
|
|
49
|
-
: {};
|
|
48
|
+
const parsed: unknown = JSON.parse(readFileSync(path, "utf-8"));
|
|
49
|
+
return isRecord(parsed) ? parsed : {};
|
|
50
50
|
} catch {
|
|
51
51
|
return {};
|
|
52
52
|
}
|
|
@@ -61,12 +61,14 @@ const shouldPersistConfigKey = (
|
|
|
61
61
|
(key !== "executor" || persistExecutor);
|
|
62
62
|
|
|
63
63
|
const applyChangedConfigValues = (
|
|
64
|
-
|
|
64
|
+
existing: RecordValue,
|
|
65
65
|
current: ConfigState,
|
|
66
66
|
changedKeys: readonly SavedConfigKey[],
|
|
67
67
|
persistAdvisor: boolean,
|
|
68
68
|
persistExecutor: boolean
|
|
69
|
-
) => {
|
|
69
|
+
): RecordValue => {
|
|
70
|
+
const dropped = new Set<string>();
|
|
71
|
+
const data: RecordValue = { ...existing };
|
|
70
72
|
for (const key of changedKeys) {
|
|
71
73
|
if (!shouldPersistConfigKey(key, persistAdvisor, persistExecutor)) {
|
|
72
74
|
continue;
|
|
@@ -74,11 +76,21 @@ const applyChangedConfigValues = (
|
|
|
74
76
|
const value = current[key];
|
|
75
77
|
const isEmptyModelRef = (key === "advisor" || key === "executor") && !value;
|
|
76
78
|
if (value === undefined || isEmptyModelRef) {
|
|
77
|
-
|
|
79
|
+
dropped.add(key);
|
|
78
80
|
} else {
|
|
79
81
|
data[key] = value;
|
|
80
82
|
}
|
|
81
83
|
}
|
|
84
|
+
if (dropped.size === 0) {
|
|
85
|
+
return data;
|
|
86
|
+
}
|
|
87
|
+
const retained: RecordValue = {};
|
|
88
|
+
for (const key of Object.keys(data)) {
|
|
89
|
+
if (!dropped.has(key)) {
|
|
90
|
+
retained[key] = data[key];
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return retained;
|
|
82
94
|
};
|
|
83
95
|
|
|
84
96
|
// Tracks the last load/save state so a later save merges fresh disk contents instead of overwriting an external edit.
|
|
@@ -86,7 +98,7 @@ let loadedConfigState: ConfigState | undefined;
|
|
|
86
98
|
let loadedConfigPath: string | undefined;
|
|
87
99
|
|
|
88
100
|
const readConfig = (path: string): AdvisorConfig => {
|
|
89
|
-
const config = JSON.parse(readFileSync(path, "
|
|
101
|
+
const config = JSON.parse(readFileSync(path, "utf-8"));
|
|
90
102
|
validateConfig(config, path);
|
|
91
103
|
return config;
|
|
92
104
|
};
|
|
@@ -142,9 +154,9 @@ export const loadConfig = (_ctx: ExtensionContext) => {
|
|
|
142
154
|
);
|
|
143
155
|
if (globalConfig) {
|
|
144
156
|
applyConfig(globalConfig);
|
|
145
|
-
const unknownKeys = unknownConfigKeys(
|
|
146
|
-
|
|
147
|
-
)
|
|
157
|
+
const unknownKeys = unknownConfigKeys(globalConfig).filter(
|
|
158
|
+
(key) => !RESERVED_ADVISOR_JSON_KEYS.has(key)
|
|
159
|
+
);
|
|
148
160
|
const warningIdentity = `${global}:${configIdentity(global)}`;
|
|
149
161
|
if (
|
|
150
162
|
unknownKeys.length > 0 &&
|
|
@@ -196,9 +208,8 @@ export const saveConfig = (
|
|
|
196
208
|
if (changedKeys.length === 0) {
|
|
197
209
|
return path;
|
|
198
210
|
}
|
|
199
|
-
const data =
|
|
200
|
-
|
|
201
|
-
data,
|
|
211
|
+
const data = applyChangedConfigValues(
|
|
212
|
+
existing,
|
|
202
213
|
current,
|
|
203
214
|
changedKeys,
|
|
204
215
|
persistAdvisor,
|
|
@@ -211,14 +222,14 @@ export const saveConfig = (
|
|
|
211
222
|
nextLoadedState.advisor = baseline
|
|
212
223
|
? baseline.advisor
|
|
213
224
|
: configuredModelRef(
|
|
214
|
-
|
|
225
|
+
isString(existing.advisor) ? existing.advisor : undefined
|
|
215
226
|
);
|
|
216
227
|
}
|
|
217
228
|
if (!persistExecutor) {
|
|
218
229
|
nextLoadedState.executor = baseline
|
|
219
230
|
? baseline.executor
|
|
220
231
|
: configuredModelRef(
|
|
221
|
-
|
|
232
|
+
isString(existing.executor) ? existing.executor : undefined
|
|
222
233
|
);
|
|
223
234
|
}
|
|
224
235
|
loadedConfigState = nextLoadedState;
|