pi-advisor-flow 0.5.0 → 0.5.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 +40 -0
- package/README.md +36 -71
- package/package.json +18 -6
- package/src/commands.ts +394 -81
- package/src/config.ts +32 -9
- package/src/model-stream.ts +10 -1
- package/src/tools.ts +78 -24
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,46 @@ All notable changes to this project are documented here.
|
|
|
4
4
|
|
|
5
5
|
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## 0.5.1
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- Updated CI and local validation to Bun 1.4.1 and kept the Pi compatibility packages at 0.84.4; Pi 0.85.0 currently adds an eager `/server` import without declaring the `@earendil-works/pi-server` dependency required by its coding-agent entry point.
|
|
12
|
+
- Kept benchmark tests out of the normal test run; use `bun run test:bench` to run them explicitly.
|
|
13
|
+
- Added Socket security dependency overrides and a `socket` script for `bunx socket optimize`.
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
- Added a repository-only failsafe benchmark with an offline replay tier, a
|
|
18
|
+
24-item decision-point corpus, a fail-closed Pi/Harbor ReactBench adapter,
|
|
19
|
+
pinned live-tier controls, hard budget limits, and task-level
|
|
20
|
+
uplift/dominance reporting. See [Benchmarking](docs/benchmark.md).
|
|
21
|
+
- Added bounded local Harbor runtime controls for Apple Container experiments,
|
|
22
|
+
Docker build-cache cleanup, and a recorded one-hour agent timeout for complex
|
|
23
|
+
ReactBench tasks.
|
|
24
|
+
- Added a disposable, credential-free GitHub build compatibility layer for
|
|
25
|
+
pinned ReactBench Dockerfiles whose Git 2.39 transport is rejected by the
|
|
26
|
+
GitHub endpoint, with bounded pre-agent Harbor infrastructure retries and
|
|
27
|
+
per-attempt artifacts for transient build and transport failures.
|
|
28
|
+
- Derived the host Harbor command timeout from the bounded agent timeout and
|
|
29
|
+
made timeout cleanup terminate the full Harbor process group, including the
|
|
30
|
+
broker and descendants.
|
|
31
|
+
- Ensured configured Advisor reasoning effort reaches the provider-facing
|
|
32
|
+
request field used by current Pi AI adapters.
|
|
33
|
+
|
|
34
|
+
### Fixed
|
|
35
|
+
|
|
36
|
+
- Made `/advisor` open the available-model picker on first use or when either saved model is missing or unavailable; activation never silently selects a model outside the user's available catalog.
|
|
37
|
+
- Added a concise `/advisor` explanation of the Advisor's second-opinion role.
|
|
38
|
+
- Preserved Markdown formatting in visible Scout and nested Advisor thinking previews while retaining their speech-bubble cue. During streaming, incomplete Markdown delimiters display transiently until they close, which is expected behavior when thinking arrives incrementally.
|
|
39
|
+
- Adopted an explicit `/model` selection made before `/advisor` as the Executor on the next successful activation, without persisting ordinary model changes while the flow is off.
|
|
40
|
+
|
|
41
|
+
## [5.1.0] - 2026-09-04 [YANKED]
|
|
42
|
+
|
|
43
|
+
### Release status
|
|
44
|
+
|
|
45
|
+
- Published by mistake instead of `0.5.1`; withdrawn from npm and removed from Git, with `latest` restored to `0.5.0`.
|
|
46
|
+
|
|
7
47
|
## 0.5.0
|
|
8
48
|
|
|
9
49
|
### Changed
|
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# pi-advisor
|
|
1
|
+
# [pi-advisor](https://github.com/philipbrembeck/pi-advisor)
|
|
2
2
|
|
|
3
3
|
<div align="center">
|
|
4
4
|
|
|
@@ -8,114 +8,79 @@ A configurable second-opinion workflow for <a href="https://github.com/earendil-
|
|
|
8
8
|
|
|
9
9
|
</div>
|
|
10
10
|
|
|
11
|
-
|
|
12
11
|
 
|
|
13
12
|
|
|
14
13
|
`pi-advisor-flow` keeps one model focused on execution and makes a second, smarter model available for consequential decisions, stalled work, and final reviews. The Executor still owns the work. The Advisor challenges assumptions, exposes risks, and suggests verification steps without taking over or running tools.
|
|
15
14
|
|
|
16
|
-
|
|
15
|
+
Keep implementation on a fast model and borrow frontier reasoning only when decisions matter. [Read why this workflow is useful](https://philipbrembeck.com/writings/2026/07/only-as-much-intelligence-as-you-need).
|
|
16
|
+
|
|
17
|
+
## How it works
|
|
17
18
|
|
|
18
|
-
|
|
19
|
+
1. The Executor works on your task as usual.
|
|
20
|
+
2. It calls `ask_advisor`, or an enabled gate starts a review.
|
|
21
|
+
3. pi-advisor reconstructs the relevant conversation and allowed repository context.
|
|
22
|
+
4. The Advisor returns an opinion. The Executor decides what to adopt, changes the code, and validates it.
|
|
19
23
|
|
|
20
|
-
|
|
21
|
-
- **Configurable review gates** before plans, after repeated failures, and before declaring completion.
|
|
22
|
-
- **Automatic loop detection** for repeated tool calls, with explicit proceed, revise, or blocked decisions.
|
|
23
|
-
- **Separate model and reasoning controls** for the Executor and Advisor.
|
|
24
|
-
- **Advisor usage accounting** with per-response token/cost details and optional cumulative direct usage in the Pi footer and session summary. Per-response usage and cost details are shown by default and can be hidden independently from the footer in `/advisor-settings`.
|
|
25
|
-
- **Privacy controls** for conversation history, repository context, explicit tracked/untracked file handoff, tool results, secret redaction, and outcome logging.
|
|
26
|
-
- **Optional persistent activation, Simple mode, session summaries, and Herdr integration.**
|
|
27
|
-
- **Compact searchable `/advisor-settings` controls** that match Pi's settings list and save changes immediately.
|
|
28
|
-
- **EXPERIMENTAL Advisor Scout** that uses the configured Executor model to curate conversation evidence before every Advisor call.
|
|
24
|
+
Regular consultations do not block execution. Automatic loop gates are different: they can stop a repeated tool action or session based on your configured failure policy.
|
|
29
25
|
|
|
30
26
|
## Install
|
|
31
27
|
|
|
32
|
-
Requires Pi 0.84.1 or later
|
|
28
|
+
Requires Pi 0.84.1 or later.
|
|
33
29
|
|
|
34
30
|
```bash
|
|
35
|
-
# npm
|
|
36
31
|
pi install npm:pi-advisor-flow
|
|
32
|
+
```
|
|
37
33
|
|
|
38
|
-
|
|
39
|
-
pi install git:github.com/philipbrembeck/pi-advisor.git
|
|
34
|
+
You can also install from GitHub:
|
|
40
35
|
|
|
41
|
-
|
|
42
|
-
pi install /
|
|
36
|
+
```bash
|
|
37
|
+
pi install git:github.com/philipbrembeck/pi-advisor.git
|
|
43
38
|
```
|
|
44
39
|
|
|
45
|
-
|
|
40
|
+
Reload Pi after installing.
|
|
46
41
|
|
|
47
42
|
## Quick start
|
|
48
43
|
|
|
49
|
-
1. Run `/advisor` to enable the flow and register `ask_advisor`.
|
|
50
|
-
2. Run `/advisor-models` to choose the Executor and Advisor models. Current model and thinking-level selections appear first and ticked, so pressing Enter keeps them.
|
|
51
|
-
3. Run `/advisor-settings` to configure review gates, context, privacy, and limits. Type to fuzzy-search settings; changes save immediately.
|
|
52
|
-
|
|
53
|
-

|
|
54
|
-
|
|
55
|
-
Unknown fields in `advisor.json` are preserved for forward compatibility and reported as non-blocking warnings. Invalid recognized values remain errors, and Advisor commands show the configuration problem without crashing their handlers.
|
|
56
|
-
|
|
57
|
-
You can also enable the flow and select both models at once:
|
|
58
|
-
|
|
59
44
|
```text
|
|
60
|
-
/advisor
|
|
45
|
+
/advisor # Enable the Advisor Flow
|
|
46
|
+
/advisor-models # Choose the Executor and Advisor models
|
|
47
|
+
/advisor-settings # Configure behavior, modes, etc.
|
|
61
48
|
```
|
|
62
49
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
1. The Executor investigates the task and forms its own candidate direction.
|
|
66
|
-
2. For a consequential decision, stalled attempt, or final review, it calls `ask_advisor` with the reconstructed conversation and allowed repository context.
|
|
67
|
-
3. When Experimental Advisor Scout is enabled, the configured Executor model selects relevant conversation groups and writes a short, explicitly untrusted synthesis.
|
|
68
|
-
4. The Advisor receives selected verbatim evidence, required current-request context, and the unchanged deterministic repository, preference, draft, and attachment regions.
|
|
69
|
-
5. The Executor decides what to adopt, performs the work, and validates the result.
|
|
50
|
+
On first use, or whenever a saved model is unavailable, `/advisor` opens the same available-model picker as `/advisor-models`; it never silently chooses an unconfigured model. After activation, `/advisor` explains that the Advisor reviews the Executor's context without changing files or running tools.
|
|
70
51
|
|
|
71
|
-
|
|
52
|
+
From the Executor, `ask_advisor({})` requests a general review. A targeted `question` or concise `draft` can focus the review on a particular decision.
|
|
72
53
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
Successful calls return an opaque `adviceId`. If global outcome logging is enabled, the Executor can call `record_advisor_outcome` once to record whether the advice was adopted and whether final validation passed.
|
|
76
|
-
|
|
77
|
-
### Experimental Advisor Scout
|
|
78
|
-
|
|
79
|
-
Experimental Advisor Scout is off by default. Enable `Experimental Advisor Scout` in the advanced `/advisor-settings` screen or set `"advisorScoutEnabled": true` in the global `advisor.json`.
|
|
80
|
-
|
|
81
|
-
Scout runs before `ask_advisor`, `/advisor-manual`, and automatic Advisor gates. It uses the configured Executor model and Executor reasoning effort in a separate model call. This adds cost and latency, but can reduce cost in the Advisor call. The compact result shows the model, selection counts, elapsed time, and usage/cost details; `Ctrl+O` shows bounded selected labels and the synthesis. Usage and cost details can be hidden in `/advisor-settings`.
|
|
82
|
-
|
|
83
|
-
Scout receives a bounded manifest of conversation and tool-history groups after the normal tool disclosure, result-cap, and redaction policies are applied. The Scout manifest has its own fixed transport limit, while the reconstructed conversation remains bounded by the Advisor's remaining context budget after repository context; manifest metadata no longer consumes that Advisor conversation budget. A zero remaining budget produces no history groups. For a pending `ask_advisor` call, Scout receives only the allowlisted question and Git-context preference, never the draft or explicit attachment paths. Scout does not receive the deterministic Git context, draft, project preferences, or explicit tracked and untracked attachments. Those regions are appended later through their existing consent and cap rules.
|
|
84
|
-
|
|
85
|
-
This experiment adapts the context-boundary idea from Zhang et al., ["FastContext: Training Efficient Repository Explorer for Coding Agents"](https://arxiv.org/html/2606.14066v1). It is not a reproduction of FastContext. pi-advisor Scout curates conversation history only.
|
|
54
|
+
In the Settings, enable the Simple Mode for a quick start.
|
|
55
|
+

|
|
86
56
|
|
|
87
57
|
## Commands
|
|
88
58
|
|
|
89
|
-
| Command
|
|
90
|
-
|
|
|
91
|
-
| `/advisor`
|
|
92
|
-
| `/advisor-manual [focus]` |
|
|
93
|
-
| `/advisor-models`
|
|
94
|
-
| `/advisor-settings`
|
|
95
|
-
| `/advisor-off`
|
|
59
|
+
| Command | What it does |
|
|
60
|
+
| ------------------------- | ------------------------------------------------- |
|
|
61
|
+
| `/advisor` | Enable the flow; choose available models when needed. |
|
|
62
|
+
| `/advisor-manual [focus]` | Ask for an immediate second opinion. |
|
|
63
|
+
| `/advisor-models` | Choose the Executor and Advisor models. |
|
|
64
|
+
| `/advisor-settings` | Configure behavior, context, privacy, and limits. |
|
|
65
|
+
| `/advisor-off` | Disable the flow and persistent activation. |
|
|
96
66
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
The Executor calls `ask_advisor({})` for a general review. It can pass a targeted `question` or a concise `draft` describing proposed work, validation, and remaining risks. If the Advisor explicitly says it cannot review a specifically named file, the Executor may make a sequential follow-up call with `includeTrackedFiles` when global consent is enabled and the file is relevant. Draft claims give the Advisor review context; they are not verification evidence.
|
|
67
|
+
### Experimental Advisor Scout
|
|
100
68
|
|
|
101
|
-
|
|
69
|
+
Advisor Scout is off by default. When enabled, the Executor model first selects relevant conversation history before the Advisor sees it. This adds a model call, latency, and cost. See the [configuration guide](https://github.com/philipbrembeck/pi-advisor/blob/main/docs/configuration.md) for details.
|
|
102
70
|
|
|
103
|
-
|
|
71
|
+
## Privacy
|
|
104
72
|
|
|
105
|
-
|
|
73
|
+
Advisor requests can include user messages, tool calls, tool results, and repository information. `/advisor-settings` controls context, tool disclosure, redaction, and explicit file handoff. Secret redaction is off by default, and tools without an explicit policy use full context. Settings are global, so a project cannot silently change them.
|
|
106
74
|
|
|
107
|
-
|
|
75
|
+
Read [Privacy and data handling](https://github.com/philipbrembeck/pi-advisor/blob/main/docs/privacy.md) before using pi-advisor with sensitive work.
|
|
108
76
|
|
|
109
77
|
## Documentation
|
|
110
78
|
|
|
111
79
|
- [Configuration and automatic loop gates](https://github.com/philipbrembeck/pi-advisor/blob/main/docs/configuration.md)
|
|
112
80
|
- [Privacy and data handling](https://github.com/philipbrembeck/pi-advisor/blob/main/docs/privacy.md)
|
|
113
81
|
- [Development](https://github.com/philipbrembeck/pi-advisor/blob/main/docs/development.md)
|
|
114
|
-
- [
|
|
115
|
-
|
|
116
|
-
## Links
|
|
117
|
-
|
|
118
|
-
- [MIT License](LICENSE)
|
|
82
|
+
- [Benchmarking](docs/benchmark.md)
|
|
119
83
|
- [Changelog](CHANGELOG.md)
|
|
84
|
+
- [MIT License](LICENSE)
|
|
120
85
|
- [npm package](https://www.npmjs.com/package/pi-advisor-flow)
|
|
121
86
|
- [Why use an Advisor flow?](https://philipbrembeck.com/writings/2026/07/only-as-much-intelligence-as-you-need)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-advisor-flow",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"description": "Advanced Executor/Advisor flow for Pi, fully configurable and extendable.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
@@ -28,33 +28,45 @@
|
|
|
28
28
|
"README.md"
|
|
29
29
|
],
|
|
30
30
|
"scripts": {
|
|
31
|
+
"bench:decisions": "bun bench/src/cli.ts decisions",
|
|
32
|
+
"bench:evaluate": "bun bench/src/cli.ts evaluate",
|
|
33
|
+
"bench:replay": "bun bench/src/cli.ts replay",
|
|
34
|
+
"bench:screen": "bun bench/src/cli.ts screen",
|
|
31
35
|
"format": "bunx ultracite fix --linter-enabled=false",
|
|
32
36
|
"lint": "bunx ultracite check",
|
|
33
37
|
"lint:fix": "bunx ultracite fix",
|
|
34
38
|
"package:check": "node scripts/check-package.mjs",
|
|
35
39
|
"prepare": "husky",
|
|
40
|
+
"socket": "bunx socket optimize",
|
|
36
41
|
"test": "bun test",
|
|
42
|
+
"test:bench": "bun --config=./bunfig.bench.toml test ./bench/test/",
|
|
37
43
|
"typecheck": "tsc --noEmit"
|
|
38
44
|
},
|
|
39
45
|
"lint-staged": {
|
|
40
46
|
"*.{json,jsonc,ts}": "bun run lint:fix --"
|
|
41
47
|
},
|
|
48
|
+
"resolutions": {
|
|
49
|
+
"is-unicode-supported": "npm:@socketregistry/is-unicode-supported@^1",
|
|
50
|
+
"safe-buffer": "npm:@socketregistry/safe-buffer@^1"
|
|
51
|
+
},
|
|
42
52
|
"overrides": {
|
|
43
53
|
"brace-expansion": "5.0.9",
|
|
54
|
+
"is-unicode-supported": "npm:@socketregistry/is-unicode-supported@^1",
|
|
55
|
+
"safe-buffer": "npm:@socketregistry/safe-buffer@^1",
|
|
44
56
|
"undici": "8.9.0"
|
|
45
57
|
},
|
|
46
58
|
"devDependencies": {
|
|
47
|
-
"@biomejs/biome": "2.5.
|
|
59
|
+
"@biomejs/biome": "2.5.12",
|
|
48
60
|
"@earendil-works/pi-ai": "^0.84.4",
|
|
49
61
|
"@earendil-works/pi-coding-agent": "^0.84.4",
|
|
50
62
|
"@earendil-works/pi-tui": "^0.84.4",
|
|
51
|
-
"@types/node": "^26.4.
|
|
52
|
-
"bun-types": "1.
|
|
63
|
+
"@types/node": "^26.4.1",
|
|
64
|
+
"bun-types": "1.4.1",
|
|
53
65
|
"husky": "^9.1.7",
|
|
54
66
|
"lint-staged": "^17.4.1",
|
|
55
|
-
"typebox": "^1.3.
|
|
67
|
+
"typebox": "^1.3.25",
|
|
56
68
|
"typescript": "^7.0.2",
|
|
57
|
-
"ultracite": "7.10.
|
|
69
|
+
"ultracite": "7.10.8"
|
|
58
70
|
},
|
|
59
71
|
"peerDependencies": {
|
|
60
72
|
"@earendil-works/pi-ai": "^0.84.1",
|
package/src/commands.ts
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
executorRef,
|
|
15
15
|
getAdvisorMaxCallsPerSession,
|
|
16
16
|
getAdvisorSettings,
|
|
17
|
+
getPersistedModelRefs,
|
|
17
18
|
isSimpleMode,
|
|
18
19
|
loadConfig,
|
|
19
20
|
parseArgs,
|
|
@@ -65,6 +66,7 @@ import {
|
|
|
65
66
|
renderAdvisorCallBox,
|
|
66
67
|
renderAdvisorResponseHeader,
|
|
67
68
|
renderScoutDetails,
|
|
69
|
+
renderThinkingMarkdown,
|
|
68
70
|
resolveAdvisorRequest,
|
|
69
71
|
ScoutStatusManager,
|
|
70
72
|
type ScoutToolDetails,
|
|
@@ -111,6 +113,20 @@ const selectedEffort = (choice: string): string | undefined => {
|
|
|
111
113
|
: choice;
|
|
112
114
|
return effort === DEFAULT_EFFORT_LEVEL ? undefined : effort;
|
|
113
115
|
};
|
|
116
|
+
const ADVISOR_ACTIVATION_EXPLANATION =
|
|
117
|
+
"The Advisor is a second-opinion model that reviews the Executor's context and returns risks, alternatives, and verification steps without changing files or running tools.";
|
|
118
|
+
const ARGUMENT_WHITESPACE = /\s+/;
|
|
119
|
+
const hasModelOverride = (args: string, key: "advisor" | "executor") =>
|
|
120
|
+
args
|
|
121
|
+
.trim()
|
|
122
|
+
.split(ARGUMENT_WHITESPACE)
|
|
123
|
+
.some((token) => {
|
|
124
|
+
const [tokenKey, value] = token.split("=");
|
|
125
|
+
return tokenKey === key && Boolean(value);
|
|
126
|
+
});
|
|
127
|
+
const hasExecutorOverride = (args: string) =>
|
|
128
|
+
hasModelOverride(args, "executor");
|
|
129
|
+
const hasAdvisorOverride = (args: string) => hasModelOverride(args, "advisor");
|
|
114
130
|
|
|
115
131
|
const CONTEXT_PRESETS: ContextPreset[] = [
|
|
116
132
|
{
|
|
@@ -246,16 +262,9 @@ class ManualAdvisorProgressComponent implements Component {
|
|
|
246
262
|
0
|
|
247
263
|
)
|
|
248
264
|
);
|
|
249
|
-
if (this.state.thinking) {
|
|
265
|
+
if (this.state.thinking?.trim()) {
|
|
250
266
|
box.addChild(
|
|
251
|
-
|
|
252
|
-
this.theme.fg(
|
|
253
|
-
"thinkingText",
|
|
254
|
-
` 💭 ${this.state.thinking.replace(/\n/g, " ").slice(-200)}`
|
|
255
|
-
),
|
|
256
|
-
0,
|
|
257
|
-
0
|
|
258
|
-
)
|
|
267
|
+
renderThinkingMarkdown(this.state.thinking.slice(-200), this.theme)
|
|
259
268
|
);
|
|
260
269
|
}
|
|
261
270
|
if (this.state.text) {
|
|
@@ -276,11 +285,205 @@ class ManualAdvisorProgressComponent implements Component {
|
|
|
276
285
|
}
|
|
277
286
|
}
|
|
278
287
|
|
|
279
|
-
const findConfiguredModel = (
|
|
288
|
+
const findConfiguredModel = (
|
|
289
|
+
ctx: ExtensionContext,
|
|
290
|
+
ref: string | undefined
|
|
291
|
+
) => {
|
|
292
|
+
if (!ref) {
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
280
295
|
const [provider, modelId] = splitRef(ref);
|
|
281
296
|
return ctx.modelRegistry.find(provider, modelId);
|
|
282
297
|
};
|
|
283
298
|
|
|
299
|
+
const getAvailableModelRefs = (ctx: ExtensionContext): string[] | undefined => {
|
|
300
|
+
if (typeof ctx.modelRegistry.getAvailable !== "function") {
|
|
301
|
+
return undefined;
|
|
302
|
+
}
|
|
303
|
+
return ctx.modelRegistry
|
|
304
|
+
.getAvailable()
|
|
305
|
+
.map((model) => `${model.provider}/${model.id}`);
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
const isSelectableModel = (
|
|
309
|
+
ctx: ExtensionContext,
|
|
310
|
+
ref: string | undefined,
|
|
311
|
+
availableRefs: Set<string> | undefined
|
|
312
|
+
) => {
|
|
313
|
+
if (!ref) {
|
|
314
|
+
return false;
|
|
315
|
+
}
|
|
316
|
+
if (availableRefs) {
|
|
317
|
+
const [provider, modelId] = splitRef(ref);
|
|
318
|
+
return availableRefs.has(`${provider}/${modelId}`);
|
|
319
|
+
}
|
|
320
|
+
return Boolean(findConfiguredModel(ctx, ref));
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
const getExplicitModelError = (
|
|
324
|
+
ctx: ExtensionContext,
|
|
325
|
+
ref: string | undefined,
|
|
326
|
+
label: "Advisor" | "Executor",
|
|
327
|
+
overridden: boolean,
|
|
328
|
+
availableRefs: Set<string> | undefined
|
|
329
|
+
) => {
|
|
330
|
+
if (overridden) {
|
|
331
|
+
if (!ref) {
|
|
332
|
+
return `${label} model not configured`;
|
|
333
|
+
}
|
|
334
|
+
if (!findConfiguredModel(ctx, ref)) {
|
|
335
|
+
return `${label} model not found: ${ref}`;
|
|
336
|
+
}
|
|
337
|
+
if (availableRefs && !isSelectableModel(ctx, ref, availableRefs)) {
|
|
338
|
+
return `${label} model unavailable: ${ref}`;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
interface ActivationModelPlan {
|
|
344
|
+
pendingExecutor: string | undefined;
|
|
345
|
+
selectAdvisor: boolean;
|
|
346
|
+
selectExecutor: boolean;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const planActivationModels = (
|
|
350
|
+
ctx: ExtensionContext,
|
|
351
|
+
executor: string | undefined,
|
|
352
|
+
advisor: string | undefined,
|
|
353
|
+
pendingExecutorRef: string | undefined,
|
|
354
|
+
persisted: ReturnType<typeof getPersistedModelRefs>,
|
|
355
|
+
executorOverride: boolean,
|
|
356
|
+
advisorOverride: boolean,
|
|
357
|
+
availableRefs: Set<string> | undefined
|
|
358
|
+
): ActivationModelPlan => {
|
|
359
|
+
const pendingExecutor =
|
|
360
|
+
!executorOverride &&
|
|
361
|
+
pendingExecutorRef &&
|
|
362
|
+
isSelectableModel(ctx, pendingExecutorRef, availableRefs)
|
|
363
|
+
? pendingExecutorRef
|
|
364
|
+
: undefined;
|
|
365
|
+
const executorConfigured =
|
|
366
|
+
executorOverride ||
|
|
367
|
+
Boolean(pendingExecutor) ||
|
|
368
|
+
Boolean(
|
|
369
|
+
persisted.executor && isSelectableModel(ctx, executor, availableRefs)
|
|
370
|
+
);
|
|
371
|
+
const advisorConfigured =
|
|
372
|
+
advisorOverride ||
|
|
373
|
+
Boolean(
|
|
374
|
+
persisted.advisor && isSelectableModel(ctx, advisor, availableRefs)
|
|
375
|
+
);
|
|
376
|
+
return {
|
|
377
|
+
pendingExecutor,
|
|
378
|
+
selectAdvisor: !advisorConfigured,
|
|
379
|
+
selectExecutor: !executorConfigured,
|
|
380
|
+
};
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
interface AdvisorModelSelection {
|
|
384
|
+
advisor: string;
|
|
385
|
+
advisorEffort: string | undefined;
|
|
386
|
+
executor: string;
|
|
387
|
+
executorEffort: string | undefined;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
interface AdvisorModelPickerOptions {
|
|
391
|
+
advisor: string | undefined;
|
|
392
|
+
advisorEffort: string | undefined;
|
|
393
|
+
executor: string | undefined;
|
|
394
|
+
executorEffort: string | undefined;
|
|
395
|
+
selectAdvisor: boolean;
|
|
396
|
+
selectExecutor: boolean;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const selectAdvisorModels = async (
|
|
400
|
+
ctx: ExtensionContext,
|
|
401
|
+
options: AdvisorModelPickerOptions
|
|
402
|
+
): Promise<AdvisorModelSelection | undefined> => {
|
|
403
|
+
if (!ctx.hasUI) {
|
|
404
|
+
return undefined;
|
|
405
|
+
}
|
|
406
|
+
const refs = getAvailableModelRefs(ctx);
|
|
407
|
+
if (refs && refs.length === 0) {
|
|
408
|
+
notify(
|
|
409
|
+
ctx,
|
|
410
|
+
"No selectable models are available. Configure a provider with /login or models.json, then retry.",
|
|
411
|
+
"error"
|
|
412
|
+
);
|
|
413
|
+
return undefined;
|
|
414
|
+
}
|
|
415
|
+
const allOptions = [
|
|
416
|
+
...new Set(
|
|
417
|
+
refs ??
|
|
418
|
+
[options.executor, options.advisor].filter((ref): ref is string =>
|
|
419
|
+
Boolean(ref)
|
|
420
|
+
)
|
|
421
|
+
),
|
|
422
|
+
];
|
|
423
|
+
let { advisor, advisorEffort, executor, executorEffort } = options;
|
|
424
|
+
|
|
425
|
+
if (options.selectExecutor) {
|
|
426
|
+
const selectedExecutor = await ctx.ui.custom<string | undefined>(
|
|
427
|
+
(tui, theme, keybindings, done) =>
|
|
428
|
+
new SearchableModelSelector({
|
|
429
|
+
allOptions,
|
|
430
|
+
currentOption: executor || undefined,
|
|
431
|
+
keybindings,
|
|
432
|
+
onCancel: () => done(undefined),
|
|
433
|
+
onSelect: done,
|
|
434
|
+
theme,
|
|
435
|
+
title: "Select Executor Model",
|
|
436
|
+
tui,
|
|
437
|
+
})
|
|
438
|
+
);
|
|
439
|
+
if (!selectedExecutor) {
|
|
440
|
+
return undefined;
|
|
441
|
+
}
|
|
442
|
+
const selectedExecutorEffort = await ctx.ui.select(
|
|
443
|
+
"Select Executor Reasoning/Thinking Level",
|
|
444
|
+
effortChoices(executorEffort)
|
|
445
|
+
);
|
|
446
|
+
if (!selectedExecutorEffort) {
|
|
447
|
+
return undefined;
|
|
448
|
+
}
|
|
449
|
+
executor = selectedExecutor;
|
|
450
|
+
executorEffort = selectedEffort(selectedExecutorEffort);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
if (options.selectAdvisor) {
|
|
454
|
+
const selectedAdvisor = await ctx.ui.custom<string | undefined>(
|
|
455
|
+
(tui, theme, keybindings, done) =>
|
|
456
|
+
new SearchableModelSelector({
|
|
457
|
+
allOptions,
|
|
458
|
+
currentOption: advisor || undefined,
|
|
459
|
+
keybindings,
|
|
460
|
+
onCancel: () => done(undefined),
|
|
461
|
+
onSelect: done,
|
|
462
|
+
theme,
|
|
463
|
+
title: "Select Advisor Model",
|
|
464
|
+
tui,
|
|
465
|
+
})
|
|
466
|
+
);
|
|
467
|
+
if (!selectedAdvisor) {
|
|
468
|
+
return undefined;
|
|
469
|
+
}
|
|
470
|
+
const selectedAdvisorEffort = await ctx.ui.select(
|
|
471
|
+
"Select Advisor Reasoning/Thinking Level",
|
|
472
|
+
effortChoices(advisorEffort)
|
|
473
|
+
);
|
|
474
|
+
if (!selectedAdvisorEffort) {
|
|
475
|
+
return undefined;
|
|
476
|
+
}
|
|
477
|
+
advisor = selectedAdvisor;
|
|
478
|
+
advisorEffort = selectedEffort(selectedAdvisorEffort);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
if (!(advisor && executor)) {
|
|
482
|
+
return undefined;
|
|
483
|
+
}
|
|
484
|
+
return { advisor, advisorEffort, executor, executorEffort };
|
|
485
|
+
};
|
|
486
|
+
|
|
284
487
|
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: one settings form maps every persisted control.
|
|
285
488
|
const applyAdvisorSettings = (settings: AdvisorSettings) => {
|
|
286
489
|
setAdvisorEffortRef(
|
|
@@ -320,7 +523,11 @@ const saveAdvisorSettings = (
|
|
|
320
523
|
settings: AdvisorSettings
|
|
321
524
|
) => {
|
|
322
525
|
applyAdvisorSettings(settings);
|
|
323
|
-
|
|
526
|
+
const persisted = getPersistedModelRefs();
|
|
527
|
+
saveConfig(ctx, {
|
|
528
|
+
persistAdvisor: Boolean(persisted.advisor),
|
|
529
|
+
persistExecutor: Boolean(persisted.executor),
|
|
530
|
+
});
|
|
324
531
|
saveGlobalOutcomeLogging(settings.outcomeLogging ?? false);
|
|
325
532
|
};
|
|
326
533
|
|
|
@@ -353,6 +560,22 @@ export const registerCommands = (
|
|
|
353
560
|
onScout,
|
|
354
561
|
undefined
|
|
355
562
|
));
|
|
563
|
+
// Pi's model_select event reports both built-in `/model` changes and direct
|
|
564
|
+
// pi.setModel() calls. Keep an inactive user selection transiently so
|
|
565
|
+
// `/advisor` can adopt it without treating every normal model change as a
|
|
566
|
+
// global default. The suppression flag covers this extension's own restore.
|
|
567
|
+
let pendingExecutorModelRef: string | undefined;
|
|
568
|
+
let suppressModelSelectionSync = false;
|
|
569
|
+
const setExecutorModel = async (
|
|
570
|
+
model: Parameters<ExtensionAPI["setModel"]>[0]
|
|
571
|
+
) => {
|
|
572
|
+
suppressModelSelectionSync = true;
|
|
573
|
+
try {
|
|
574
|
+
return await pi.setModel(model);
|
|
575
|
+
} finally {
|
|
576
|
+
suppressModelSelectionSync = false;
|
|
577
|
+
}
|
|
578
|
+
};
|
|
356
579
|
const manualConsultations = new Map<AbortController, symbol>();
|
|
357
580
|
const manualProgressTimers = new Map<
|
|
358
581
|
AbortController,
|
|
@@ -518,17 +741,25 @@ export const registerCommands = (
|
|
|
518
741
|
const resolveActivationModels = async (ctx: ExtensionContext) => {
|
|
519
742
|
const executor = findConfiguredModel(ctx, executorRef);
|
|
520
743
|
if (!executor) {
|
|
521
|
-
return {
|
|
744
|
+
return {
|
|
745
|
+
error: executorRef
|
|
746
|
+
? `Executor model not found: ${executorRef}`
|
|
747
|
+
: "Executor model not configured",
|
|
748
|
+
};
|
|
522
749
|
}
|
|
523
750
|
const advisor = findConfiguredModel(ctx, advisorRef);
|
|
524
751
|
if (!advisor) {
|
|
525
|
-
return {
|
|
752
|
+
return {
|
|
753
|
+
error: advisorRef
|
|
754
|
+
? `Advisor model not found: ${advisorRef}`
|
|
755
|
+
: "Advisor model not configured",
|
|
756
|
+
};
|
|
526
757
|
}
|
|
527
758
|
const advisorAuth = await ctx.modelRegistry.getApiKeyAndHeaders(advisor);
|
|
528
759
|
if (!(advisorAuth.ok && advisorAuth.apiKey)) {
|
|
529
760
|
return { error: `No API key for Advisor ${advisorRef}` };
|
|
530
761
|
}
|
|
531
|
-
if (!(await
|
|
762
|
+
if (!(await setExecutorModel(executor))) {
|
|
532
763
|
return { error: `No API key for Executor ${executorRef}` };
|
|
533
764
|
}
|
|
534
765
|
return {};
|
|
@@ -549,6 +780,83 @@ export const registerCommands = (
|
|
|
549
780
|
}
|
|
550
781
|
};
|
|
551
782
|
|
|
783
|
+
const prepareActivationModels = async (
|
|
784
|
+
ctx: ExtensionContext,
|
|
785
|
+
announce: boolean,
|
|
786
|
+
executorOverride: boolean,
|
|
787
|
+
advisorOverride: boolean
|
|
788
|
+
): Promise<
|
|
789
|
+
{ pickedModels: boolean; pendingExecutor?: string } | undefined
|
|
790
|
+
> => {
|
|
791
|
+
const persisted = getPersistedModelRefs();
|
|
792
|
+
const availableRefs = getAvailableModelRefs(ctx);
|
|
793
|
+
const availableRefSet = availableRefs ? new Set(availableRefs) : undefined;
|
|
794
|
+
const explicitError =
|
|
795
|
+
getExplicitModelError(
|
|
796
|
+
ctx,
|
|
797
|
+
executorRef,
|
|
798
|
+
"Executor",
|
|
799
|
+
executorOverride,
|
|
800
|
+
availableRefSet
|
|
801
|
+
) ??
|
|
802
|
+
getExplicitModelError(
|
|
803
|
+
ctx,
|
|
804
|
+
advisorRef,
|
|
805
|
+
"Advisor",
|
|
806
|
+
advisorOverride,
|
|
807
|
+
availableRefSet
|
|
808
|
+
);
|
|
809
|
+
if (explicitError) {
|
|
810
|
+
notify(ctx, explicitError, "error");
|
|
811
|
+
return;
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
const plan = planActivationModels(
|
|
815
|
+
ctx,
|
|
816
|
+
executorRef,
|
|
817
|
+
advisorRef,
|
|
818
|
+
pendingExecutorModelRef,
|
|
819
|
+
persisted,
|
|
820
|
+
executorOverride,
|
|
821
|
+
advisorOverride,
|
|
822
|
+
availableRefSet
|
|
823
|
+
);
|
|
824
|
+
setExecutorRef(plan.pendingExecutor ?? executorRef);
|
|
825
|
+
if (!(plan.selectExecutor || plan.selectAdvisor)) {
|
|
826
|
+
return { pendingExecutor: plan.pendingExecutor, pickedModels: false };
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
// Always-on startup cannot open an interactive picker, so it leaves the
|
|
830
|
+
// flow disabled until the user selects both models with `/advisor`.
|
|
831
|
+
if (!announce) {
|
|
832
|
+
notify(
|
|
833
|
+
ctx,
|
|
834
|
+
"Advisor models are not configured or available. Run /advisor to choose them.",
|
|
835
|
+
"error"
|
|
836
|
+
);
|
|
837
|
+
return;
|
|
838
|
+
}
|
|
839
|
+
const selection = await selectAdvisorModels(ctx, {
|
|
840
|
+
advisor: advisorOverride || persisted.advisor ? advisorRef : "",
|
|
841
|
+
advisorEffort: advisorEffortRef,
|
|
842
|
+
executor:
|
|
843
|
+
executorOverride || plan.pendingExecutor || persisted.executor
|
|
844
|
+
? executorRef
|
|
845
|
+
: "",
|
|
846
|
+
executorEffort: executorEffortRef,
|
|
847
|
+
selectAdvisor: plan.selectAdvisor,
|
|
848
|
+
selectExecutor: plan.selectExecutor,
|
|
849
|
+
});
|
|
850
|
+
if (!selection) {
|
|
851
|
+
return;
|
|
852
|
+
}
|
|
853
|
+
setAdvisorRef(selection.advisor);
|
|
854
|
+
setAdvisorEffortRef(selection.advisorEffort);
|
|
855
|
+
setExecutorRef(selection.executor);
|
|
856
|
+
setExecutorEffortRef(selection.executorEffort);
|
|
857
|
+
return { pendingExecutor: plan.pendingExecutor, pickedModels: true };
|
|
858
|
+
};
|
|
859
|
+
|
|
552
860
|
const activateAdvisor = async (
|
|
553
861
|
args: string,
|
|
554
862
|
ctx: ExtensionContext,
|
|
@@ -559,32 +867,53 @@ export const registerCommands = (
|
|
|
559
867
|
}
|
|
560
868
|
const previous = {
|
|
561
869
|
advisor: advisorRef,
|
|
870
|
+
advisorEffort: advisorEffortRef,
|
|
562
871
|
contextMaxChars: contextMaxCharsRef,
|
|
563
872
|
executor: executorRef,
|
|
873
|
+
executorEffort: executorEffortRef,
|
|
564
874
|
};
|
|
565
875
|
const restoreRefs = () => {
|
|
566
876
|
setAdvisorRef(previous.advisor);
|
|
877
|
+
setAdvisorEffortRef(previous.advisorEffort);
|
|
567
878
|
setContextMaxCharsRef(previous.contextMaxChars);
|
|
568
879
|
setExecutorRef(previous.executor);
|
|
880
|
+
setExecutorEffortRef(previous.executorEffort);
|
|
569
881
|
};
|
|
882
|
+
const executorOverride = hasExecutorOverride(args);
|
|
883
|
+
const advisorOverride = hasAdvisorOverride(args);
|
|
570
884
|
const argumentError = parseArgs(args);
|
|
571
885
|
if (argumentError) {
|
|
572
886
|
restoreRefs();
|
|
573
887
|
notify(ctx, argumentError, "error");
|
|
574
888
|
return;
|
|
575
889
|
}
|
|
890
|
+
|
|
891
|
+
const prepared = await prepareActivationModels(
|
|
892
|
+
ctx,
|
|
893
|
+
announce,
|
|
894
|
+
executorOverride,
|
|
895
|
+
advisorOverride
|
|
896
|
+
);
|
|
897
|
+
if (!prepared) {
|
|
898
|
+
restoreRefs();
|
|
899
|
+
return;
|
|
900
|
+
}
|
|
576
901
|
const { error } = await resolveActivationModels(ctx);
|
|
577
902
|
if (error) {
|
|
578
903
|
restoreRefs();
|
|
579
904
|
notify(ctx, error, "error");
|
|
580
905
|
return;
|
|
581
906
|
}
|
|
582
|
-
// parseArgs
|
|
583
|
-
//
|
|
907
|
+
// parseArgs, model picking, and an inactive `/model` selection only mutate
|
|
908
|
+
// in-memory refs. Persist them once both models are known and authenticated,
|
|
584
909
|
// so an unusable model reference is never written to the configuration.
|
|
585
|
-
if (args.trim()) {
|
|
586
|
-
saveConfig(ctx);
|
|
910
|
+
if (args.trim() || prepared.pickedModels || prepared.pendingExecutor) {
|
|
911
|
+
saveConfig(ctx, { persistAdvisor: true, persistExecutor: true });
|
|
587
912
|
}
|
|
913
|
+
// A successful activation has committed the effective Executor. Do not let
|
|
914
|
+
// an older inactive selection override an explicit activation argument on a
|
|
915
|
+
// later attempt.
|
|
916
|
+
pendingExecutorModelRef = undefined;
|
|
588
917
|
if (executorEffortRef) {
|
|
589
918
|
pi.setThinkingLevel(executorEffortRef as ThinkingLevel);
|
|
590
919
|
}
|
|
@@ -598,7 +927,7 @@ export const registerCommands = (
|
|
|
598
927
|
if (announce) {
|
|
599
928
|
notify(
|
|
600
929
|
ctx,
|
|
601
|
-
|
|
930
|
+
`${ADVISOR_ACTIVATION_EXPLANATION}\n\nAdvisor flow ready — Executor: ${executorRef} (thinking: ${executorEffortRef || "default"}) · Advisor: ${advisorRef} (thinking: ${advisorEffortRef || "default"})`,
|
|
602
931
|
"info"
|
|
603
932
|
);
|
|
604
933
|
}
|
|
@@ -665,6 +994,7 @@ export const registerCommands = (
|
|
|
665
994
|
);
|
|
666
995
|
|
|
667
996
|
pi.on("session_start", async (_event, ctx) => {
|
|
997
|
+
pendingExecutorModelRef = undefined;
|
|
668
998
|
// A malformed advisor.json or a provider auth failure must not reject a
|
|
669
999
|
// lifecycle handler and break session startup.
|
|
670
1000
|
try {
|
|
@@ -679,17 +1009,29 @@ export const registerCommands = (
|
|
|
679
1009
|
});
|
|
680
1010
|
|
|
681
1011
|
pi.on("model_select", (event, ctx) => {
|
|
682
|
-
//
|
|
683
|
-
//
|
|
684
|
-
|
|
1012
|
+
// "restore" replays a stored session model and "cycle" changes the active
|
|
1013
|
+
// model without an explicit `/model` choice. Neither should redefine the
|
|
1014
|
+
// configured Executor.
|
|
1015
|
+
if (event.source !== "set" || suppressModelSelectionSync) {
|
|
685
1016
|
return;
|
|
686
1017
|
}
|
|
687
1018
|
const selected = `${event.model.provider}/${event.model.id}`;
|
|
1019
|
+
if (!flowEnabled()) {
|
|
1020
|
+
// Defer persistence until `/advisor` succeeds. This keeps ordinary model
|
|
1021
|
+
// selection global defaults untouched when the flow is not enabled.
|
|
1022
|
+
pendingExecutorModelRef = selected;
|
|
1023
|
+
return;
|
|
1024
|
+
}
|
|
1025
|
+
pendingExecutorModelRef = undefined;
|
|
688
1026
|
if (selected === executorRef) {
|
|
689
1027
|
return;
|
|
690
1028
|
}
|
|
1029
|
+
const persisted = getPersistedModelRefs();
|
|
691
1030
|
setExecutorRef(selected);
|
|
692
|
-
saveConfig(ctx
|
|
1031
|
+
saveConfig(ctx, {
|
|
1032
|
+
persistAdvisor: Boolean(persisted.advisor),
|
|
1033
|
+
persistExecutor: true,
|
|
1034
|
+
});
|
|
693
1035
|
});
|
|
694
1036
|
|
|
695
1037
|
pi.on("session_shutdown", (_event, ctx) => {
|
|
@@ -805,7 +1147,7 @@ export const registerCommands = (
|
|
|
805
1147
|
|
|
806
1148
|
pi.registerCommand("advisor", {
|
|
807
1149
|
description:
|
|
808
|
-
"Enable the Executor/Advisor flow and switch to the configured Executor model; accepts contextMaxChars=N",
|
|
1150
|
+
"Enable the Executor/Advisor flow and switch to the configured or explicitly selected Executor model; accepts contextMaxChars=N",
|
|
809
1151
|
handler: (args, ctx) => activateAdvisor(args, ctx),
|
|
810
1152
|
});
|
|
811
1153
|
|
|
@@ -816,66 +1158,33 @@ export const registerCommands = (
|
|
|
816
1158
|
if (!(loadCommandConfig(ctx) && ctx.hasUI)) {
|
|
817
1159
|
return;
|
|
818
1160
|
}
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
const
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
tui,
|
|
834
|
-
})
|
|
835
|
-
);
|
|
836
|
-
if (!executor) {
|
|
837
|
-
return;
|
|
838
|
-
}
|
|
839
|
-
|
|
840
|
-
const executorEffort = await ctx.ui.select(
|
|
841
|
-
"Select Executor Reasoning/Thinking Level",
|
|
842
|
-
effortChoices(executorEffortRef)
|
|
843
|
-
);
|
|
844
|
-
if (!executorEffort) {
|
|
845
|
-
return;
|
|
846
|
-
}
|
|
847
|
-
|
|
848
|
-
const advisor = await ctx.ui.custom<string | undefined>(
|
|
849
|
-
(tui, theme, keybindings, done) =>
|
|
850
|
-
new SearchableModelSelector({
|
|
851
|
-
allOptions: refs,
|
|
852
|
-
currentOption: advisorRef,
|
|
853
|
-
keybindings,
|
|
854
|
-
onCancel: () => done(undefined),
|
|
855
|
-
onSelect: done,
|
|
856
|
-
theme,
|
|
857
|
-
title: "Select Advisor Model",
|
|
858
|
-
tui,
|
|
859
|
-
})
|
|
860
|
-
);
|
|
861
|
-
if (!advisor) {
|
|
862
|
-
return;
|
|
863
|
-
}
|
|
864
|
-
|
|
865
|
-
const advisorEffort = await ctx.ui.select(
|
|
866
|
-
"Select Advisor Reasoning/Thinking Level",
|
|
867
|
-
effortChoices(advisorEffortRef)
|
|
868
|
-
);
|
|
869
|
-
if (!advisorEffort) {
|
|
1161
|
+
// When `/model` was used before activation, show that session choice as
|
|
1162
|
+
// the Executor's current option instead of making the persisted Executor
|
|
1163
|
+
// look like the active selection.
|
|
1164
|
+
const persisted = getPersistedModelRefs();
|
|
1165
|
+
const selection = await selectAdvisorModels(ctx, {
|
|
1166
|
+
advisor: persisted.advisor ? advisorRef : "",
|
|
1167
|
+
advisorEffort: advisorEffortRef,
|
|
1168
|
+
executor:
|
|
1169
|
+
pendingExecutorModelRef ?? (persisted.executor ? executorRef : ""),
|
|
1170
|
+
executorEffort: executorEffortRef,
|
|
1171
|
+
selectAdvisor: true,
|
|
1172
|
+
selectExecutor: true,
|
|
1173
|
+
});
|
|
1174
|
+
if (!selection) {
|
|
870
1175
|
return;
|
|
871
1176
|
}
|
|
872
1177
|
|
|
873
|
-
setExecutorRef(executor);
|
|
874
|
-
setAdvisorRef(advisor);
|
|
875
|
-
setExecutorEffortRef(
|
|
876
|
-
setAdvisorEffortRef(
|
|
1178
|
+
setExecutorRef(selection.executor);
|
|
1179
|
+
setAdvisorRef(selection.advisor);
|
|
1180
|
+
setExecutorEffortRef(selection.executorEffort);
|
|
1181
|
+
setAdvisorEffortRef(selection.advisorEffort);
|
|
877
1182
|
|
|
878
|
-
const path = saveConfig(ctx
|
|
1183
|
+
const path = saveConfig(ctx, {
|
|
1184
|
+
persistAdvisor: true,
|
|
1185
|
+
persistExecutor: true,
|
|
1186
|
+
});
|
|
1187
|
+
pendingExecutorModelRef = undefined;
|
|
879
1188
|
ctx.ui.notify(
|
|
880
1189
|
`Saved Executor + Advisor configurations to ${path}`,
|
|
881
1190
|
"info"
|
|
@@ -932,8 +1241,12 @@ export const registerCommands = (
|
|
|
932
1241
|
// Leaving alwaysOn set would silently reactivate the flow next session.
|
|
933
1242
|
const wasAlwaysOn = alwaysOnRef;
|
|
934
1243
|
if (wasAlwaysOn) {
|
|
1244
|
+
const persisted = getPersistedModelRefs();
|
|
935
1245
|
setAlwaysOnRef(false);
|
|
936
|
-
saveConfig(ctx
|
|
1246
|
+
saveConfig(ctx, {
|
|
1247
|
+
persistAdvisor: Boolean(persisted.advisor),
|
|
1248
|
+
persistExecutor: Boolean(persisted.executor),
|
|
1249
|
+
});
|
|
937
1250
|
}
|
|
938
1251
|
notify(
|
|
939
1252
|
ctx,
|
package/src/config.ts
CHANGED
|
@@ -13,8 +13,6 @@ import {
|
|
|
13
13
|
isValidGitContextLevel,
|
|
14
14
|
} from "./git.js";
|
|
15
15
|
|
|
16
|
-
export const FALLBACK_EXECUTOR = "openai-codex/gpt-5.6-luna";
|
|
17
|
-
export const FALLBACK_ADVISOR = "openai-codex/gpt-5.6-sol";
|
|
18
16
|
export const DEFAULT_CONTEXT_MAX_CHARS = 15_000;
|
|
19
17
|
export const MAX_CONTEXT_MAX_CHARS = Number.MAX_SAFE_INTEGER;
|
|
20
18
|
export const DEFAULT_ADVISOR_TOOL_RESULT_MAX_LINES = DEFAULT_MAX_LINES;
|
|
@@ -37,8 +35,11 @@ export const GATE_FAILURE_MODES: GateFailureMode[] = [
|
|
|
37
35
|
"warn-and-continue",
|
|
38
36
|
];
|
|
39
37
|
|
|
40
|
-
|
|
41
|
-
export let
|
|
38
|
+
// An empty ref means no model has been selected yet.
|
|
39
|
+
export let executorRef = "";
|
|
40
|
+
export let advisorRef = "";
|
|
41
|
+
let persistedExecutorRef: string | undefined;
|
|
42
|
+
let persistedAdvisorRef: string | undefined;
|
|
42
43
|
export let executorEffortRef: string | undefined;
|
|
43
44
|
export let advisorEffortRef: string | undefined;
|
|
44
45
|
export let contextMaxCharsRef = DEFAULT_CONTEXT_MAX_CHARS;
|
|
@@ -75,6 +76,11 @@ export const setExecutorRef = (ref: string) => {
|
|
|
75
76
|
export const setAdvisorRef = (ref: string) => {
|
|
76
77
|
advisorRef = ref;
|
|
77
78
|
};
|
|
79
|
+
/** Returns model refs explicitly persisted in the global Advisor config. */
|
|
80
|
+
export const getPersistedModelRefs = () => ({
|
|
81
|
+
advisor: persistedAdvisorRef,
|
|
82
|
+
executor: persistedExecutorRef,
|
|
83
|
+
});
|
|
78
84
|
export const setExecutorEffortRef = (effort: string | undefined) => {
|
|
79
85
|
executorEffortRef = effort;
|
|
80
86
|
};
|
|
@@ -469,8 +475,10 @@ export const validateConfig = (
|
|
|
469
475
|
};
|
|
470
476
|
|
|
471
477
|
const resetDefaults = () => {
|
|
472
|
-
executorRef =
|
|
473
|
-
advisorRef =
|
|
478
|
+
executorRef = "";
|
|
479
|
+
advisorRef = "";
|
|
480
|
+
persistedExecutorRef = undefined;
|
|
481
|
+
persistedAdvisorRef = undefined;
|
|
474
482
|
executorEffortRef = undefined;
|
|
475
483
|
advisorEffortRef = undefined;
|
|
476
484
|
contextMaxCharsRef = DEFAULT_CONTEXT_MAX_CHARS;
|
|
@@ -522,6 +530,9 @@ const applyNonEmptyStringConfig = (
|
|
|
522
530
|
}
|
|
523
531
|
};
|
|
524
532
|
|
|
533
|
+
const configuredModelRef = (value: string | undefined): string | undefined =>
|
|
534
|
+
value?.trim() || undefined;
|
|
535
|
+
|
|
525
536
|
const applyConfig = (config: AdvisorConfig) => {
|
|
526
537
|
applyNonEmptyStringConfig(config.executor, setExecutorRef);
|
|
527
538
|
applyNonEmptyStringConfig(config.advisor, setAdvisorRef);
|
|
@@ -667,6 +678,8 @@ export const loadConfig = (_ctx: ExtensionContext) => {
|
|
|
667
678
|
const globalConfig = existsSync(global)
|
|
668
679
|
? readConfigCached(global)
|
|
669
680
|
: undefined;
|
|
681
|
+
persistedExecutorRef = configuredModelRef(globalConfig?.executor);
|
|
682
|
+
persistedAdvisorRef = configuredModelRef(globalConfig?.advisor);
|
|
670
683
|
if (globalConfig) {
|
|
671
684
|
applyConfig(globalConfig);
|
|
672
685
|
const unknownKeys = unknownConfigKeys(globalConfig as ConfigRecord);
|
|
@@ -691,8 +704,18 @@ export const loadConfig = (_ctx: ExtensionContext) => {
|
|
|
691
704
|
};
|
|
692
705
|
|
|
693
706
|
/** Saves user-controlled configuration globally without outcome consent. */
|
|
694
|
-
export
|
|
707
|
+
export interface SaveConfigOptions {
|
|
708
|
+
persistAdvisor?: boolean;
|
|
709
|
+
persistExecutor?: boolean;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
export const saveConfig = (
|
|
713
|
+
_ctx: ExtensionContext,
|
|
714
|
+
options: SaveConfigOptions = {}
|
|
715
|
+
) => {
|
|
695
716
|
const path = join(getAgentDir(), "advisor.json");
|
|
717
|
+
const persistAdvisor = options.persistAdvisor ?? true;
|
|
718
|
+
const persistExecutor = options.persistExecutor ?? true;
|
|
696
719
|
let existing: Record<string, unknown> = {};
|
|
697
720
|
try {
|
|
698
721
|
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
@@ -707,7 +730,7 @@ export const saveConfig = (_ctx: ExtensionContext) => {
|
|
|
707
730
|
}
|
|
708
731
|
const data = {
|
|
709
732
|
...existing,
|
|
710
|
-
advisor: advisorRef,
|
|
733
|
+
...(persistAdvisor && advisorRef ? { advisor: advisorRef } : {}),
|
|
711
734
|
advisorAutoLoopGate: advisorAutoLoopGateRef,
|
|
712
735
|
advisorBlockOnBlocked: advisorBlockOnBlockedRef,
|
|
713
736
|
advisorCollapseResponses: advisorCollapseResponsesRef,
|
|
@@ -718,7 +741,7 @@ export const saveConfig = (_ctx: ExtensionContext) => {
|
|
|
718
741
|
advisorLoopThreshold: advisorLoopThresholdRef,
|
|
719
742
|
advisorPlanGate: advisorPlanGateRef,
|
|
720
743
|
contextMaxChars: contextMaxCharsRef,
|
|
721
|
-
executor: executorRef,
|
|
744
|
+
...(persistExecutor && executorRef ? { executor: executorRef } : {}),
|
|
722
745
|
executorEffort: executorEffortRef,
|
|
723
746
|
...(advisorMaxCallsPerSessionRef === undefined
|
|
724
747
|
? {}
|
package/src/model-stream.ts
CHANGED
|
@@ -18,9 +18,12 @@ export interface ResolvedConfiguredModel {
|
|
|
18
18
|
|
|
19
19
|
export const resolveConfiguredModel = async (
|
|
20
20
|
ctx: ExtensionContext,
|
|
21
|
-
ref: string,
|
|
21
|
+
ref: string | undefined,
|
|
22
22
|
label: string
|
|
23
23
|
): Promise<ResolvedConfiguredModel> => {
|
|
24
|
+
if (!ref) {
|
|
25
|
+
throw new Error(`${label} model not configured`);
|
|
26
|
+
}
|
|
24
27
|
const [provider, modelId] = splitRef(ref);
|
|
25
28
|
const model = ctx.modelRegistry.find(provider, modelId);
|
|
26
29
|
if (!model) {
|
|
@@ -193,7 +196,13 @@ export const collectTextStream = async (
|
|
|
193
196
|
apiKey: resolved.apiKey,
|
|
194
197
|
env: resolved.env,
|
|
195
198
|
headers: resolved.headers,
|
|
199
|
+
// `stream()` uses the provider-facing name while the extension's public
|
|
200
|
+
// option keeps the Pi-facing `reasoning` name. Preserve both so the
|
|
201
|
+
// configured effort reaches providers that serialize reasoning_effort.
|
|
196
202
|
reasoning: options.reasoning as never,
|
|
203
|
+
...(options.reasoning === undefined
|
|
204
|
+
? {}
|
|
205
|
+
: { reasoningEffort: options.reasoning as never }),
|
|
197
206
|
signal: options.signal,
|
|
198
207
|
}
|
|
199
208
|
);
|
package/src/tools.ts
CHANGED
|
@@ -10,7 +10,14 @@ import {
|
|
|
10
10
|
type ToolCallEventResult,
|
|
11
11
|
type ToolRenderResultOptions,
|
|
12
12
|
} from "@earendil-works/pi-coding-agent";
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
Box,
|
|
15
|
+
type Component,
|
|
16
|
+
Markdown,
|
|
17
|
+
Text,
|
|
18
|
+
truncateToWidth,
|
|
19
|
+
visibleWidth,
|
|
20
|
+
} from "@earendil-works/pi-tui";
|
|
14
21
|
import { Type } from "typebox";
|
|
15
22
|
import {
|
|
16
23
|
advisorAutoLoopGateRef,
|
|
@@ -111,6 +118,56 @@ export const SPINNER_FRAMES = [
|
|
|
111
118
|
"⠇",
|
|
112
119
|
"⠏",
|
|
113
120
|
];
|
|
121
|
+
|
|
122
|
+
const THINKING_PREFIX = " 💭 ";
|
|
123
|
+
const THINKING_PREFIX_WIDTH = visibleWidth(THINKING_PREFIX);
|
|
124
|
+
type ThinkingTheme = Pick<Theme, "fg">;
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Renders visible nested-model thinking with the same Markdown semantics as
|
|
128
|
+
* Pi's assistant thinking blocks while keeping the compact speech-bubble cue.
|
|
129
|
+
* The prefix is added after Markdown parsing so it cannot change block syntax.
|
|
130
|
+
*
|
|
131
|
+
* Note: During streaming, incomplete Markdown (e.g., `**text` without closing `**`)
|
|
132
|
+
* displays raw markers transiently until delimiters arrive. This mirrors expected
|
|
133
|
+
* behavior when typing Markdown incrementally and is acceptable in the context
|
|
134
|
+
* of brief thinking previews. When thinking is complete, all markers render.
|
|
135
|
+
*/
|
|
136
|
+
class ThinkingMarkdown implements Component {
|
|
137
|
+
private readonly markdown: Markdown;
|
|
138
|
+
private readonly prefix: string;
|
|
139
|
+
|
|
140
|
+
constructor(thinking: string, theme: ThinkingTheme) {
|
|
141
|
+
this.markdown = new Markdown(thinking.trim(), 0, 0, getMarkdownTheme(), {
|
|
142
|
+
color: (text) => theme.fg("thinkingText", text),
|
|
143
|
+
italic: true,
|
|
144
|
+
});
|
|
145
|
+
this.prefix = theme.fg("thinkingText", THINKING_PREFIX);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
render(width: number): string[] {
|
|
149
|
+
const renderWidth = Math.max(1, Math.floor(width));
|
|
150
|
+
const contentWidth = Math.max(1, renderWidth - THINKING_PREFIX_WIDTH);
|
|
151
|
+
const lines = this.markdown.render(contentWidth);
|
|
152
|
+
return lines.map((line, index) =>
|
|
153
|
+
truncateToWidth(
|
|
154
|
+
index === 0 ? `${this.prefix}${line}` : line,
|
|
155
|
+
renderWidth,
|
|
156
|
+
""
|
|
157
|
+
)
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
invalidate(): void {
|
|
162
|
+
this.markdown.invalidate();
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export const renderThinkingMarkdown = (
|
|
167
|
+
thinking: string,
|
|
168
|
+
theme: ThinkingTheme
|
|
169
|
+
): Component => new ThinkingMarkdown(thinking, theme);
|
|
170
|
+
|
|
114
171
|
export const resolveAdvisorRequest = (question?: string) =>
|
|
115
172
|
question?.trim() || undefined;
|
|
116
173
|
export const advisorMessageText = (
|
|
@@ -1214,21 +1271,19 @@ export const renderScoutDetails = (
|
|
|
1214
1271
|
if (scout.fallbackReason) {
|
|
1215
1272
|
lines.push(theme.fg("warning", ` ${scout.fallbackReason}`));
|
|
1216
1273
|
}
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
` 💭 ${scout.thinking.replace(/\n/g, " ").slice(-200)}`
|
|
1222
|
-
)
|
|
1223
|
-
);
|
|
1274
|
+
const thinking = scout.thinking && active ? scout.thinking.slice(-200) : "";
|
|
1275
|
+
box.addChild(new Text(lines.join("\n"), 0, 0));
|
|
1276
|
+
if (thinking.trim()) {
|
|
1277
|
+
box.addChild(renderThinkingMarkdown(thinking, theme));
|
|
1224
1278
|
}
|
|
1279
|
+
const expandedLines: string[] = [];
|
|
1225
1280
|
if (expanded && scout.selectedLabels?.length) {
|
|
1226
|
-
|
|
1281
|
+
expandedLines.push(
|
|
1227
1282
|
theme.fg("dim", ` Selected: ${scout.selectedLabels.join("; ")}`)
|
|
1228
1283
|
);
|
|
1229
1284
|
}
|
|
1230
1285
|
if (expanded && scout.synthesis) {
|
|
1231
|
-
|
|
1286
|
+
expandedLines.push(
|
|
1232
1287
|
theme.fg(
|
|
1233
1288
|
"dim",
|
|
1234
1289
|
` Scout synthesis (untrusted inference): ${scout.synthesis}`
|
|
@@ -1236,14 +1291,16 @@ export const renderScoutDetails = (
|
|
|
1236
1291
|
);
|
|
1237
1292
|
}
|
|
1238
1293
|
if (expanded && scout.omittedBeforeScout) {
|
|
1239
|
-
|
|
1294
|
+
expandedLines.push(
|
|
1240
1295
|
theme.fg(
|
|
1241
1296
|
"dim",
|
|
1242
1297
|
` ${scout.omittedBeforeScout} group(s) omitted before Scout`
|
|
1243
1298
|
)
|
|
1244
1299
|
);
|
|
1245
1300
|
}
|
|
1246
|
-
|
|
1301
|
+
if (expandedLines.length > 0) {
|
|
1302
|
+
box.addChild(new Text(expandedLines.join("\n"), 0, 0));
|
|
1303
|
+
}
|
|
1247
1304
|
};
|
|
1248
1305
|
|
|
1249
1306
|
const syncRenderPhase = (context: AdvisorToolContext, phase: string) => {
|
|
@@ -1283,14 +1340,14 @@ const renderPartialAdvisorResult = (
|
|
|
1283
1340
|
const lines = [
|
|
1284
1341
|
`${theme.fg("warning", theme.bold(`◆ ADVISOR ${frame}`))} ${theme.fg("dim", "· Working…")}`,
|
|
1285
1342
|
];
|
|
1286
|
-
|
|
1343
|
+
box.addChild(new Text(lines.join("\n"), 0, 0));
|
|
1344
|
+
if (details?.thinking?.trim()) {
|
|
1287
1345
|
const thought =
|
|
1288
1346
|
details.thinking.length > 200
|
|
1289
1347
|
? details.thinking.slice(-200)
|
|
1290
1348
|
: details.thinking;
|
|
1291
|
-
|
|
1349
|
+
box.addChild(renderThinkingMarkdown(thought, theme));
|
|
1292
1350
|
}
|
|
1293
|
-
box.addChild(new Text(lines.join("\n"), 0, 0));
|
|
1294
1351
|
if (details?.text) {
|
|
1295
1352
|
box.addChild(
|
|
1296
1353
|
new Markdown(
|
|
@@ -1354,17 +1411,14 @@ const renderFinalAdvisorResult = (
|
|
|
1354
1411
|
if (attachments.length) {
|
|
1355
1412
|
lines.push(theme.fg("dim", ` ${attachments.join(" · ")}`));
|
|
1356
1413
|
}
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
theme.fg(
|
|
1361
|
-
"thinkingText",
|
|
1362
|
-
` 💭 ${thought}${details.thinking.length > 300 ? "…" : ""}`
|
|
1363
|
-
)
|
|
1364
|
-
);
|
|
1365
|
-
}
|
|
1414
|
+
const thinking = details?.thinking?.trim()
|
|
1415
|
+
? `${details.thinking.slice(0, 300)}${details.thinking.length > 300 ? "…" : ""}`
|
|
1416
|
+
: "";
|
|
1366
1417
|
const displayAdvice = advice || "(Advisor returned no advice.)";
|
|
1367
1418
|
box.addChild(new Text(lines.join("\n"), 0, 0));
|
|
1419
|
+
if (thinking) {
|
|
1420
|
+
box.addChild(renderThinkingMarkdown(thinking, theme));
|
|
1421
|
+
}
|
|
1368
1422
|
box.addChild(
|
|
1369
1423
|
new Markdown(
|
|
1370
1424
|
adviceForDisplay(displayAdvice, expanded),
|