pi-advisor-flow 0.3.0 → 0.3.3
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 +21 -0
- package/README.md +21 -5
- package/extensions/index.ts +4 -2
- package/package.json +7 -7
- package/src/commands.ts +74 -22
- package/src/config.ts +11 -0
- package/src/conversation.ts +1 -1
- package/src/model-stream.ts +106 -0
- package/src/scout-context.ts +526 -0
- package/src/scout.ts +360 -0
- package/src/tools.ts +431 -73
- package/src/ui.ts +23 -3
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,27 @@ 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.3.3
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- Added **EXPERIMENTAL** Advisor Scout, an opt-in conversation curator that uses the configured Executor model before every Advisor invocation. It keeps selected evidence verbatim, labels its synthesis as untrusted inference, reports separate usage and latency, and falls back to the original context without changing Advisor or gate safety behavior. The experiment adapts the context-boundary idea from the [FastContext paper](https://arxiv.org/html/2606.14066v1); it is not the paper's repository explorer and does not claim its reported effect size.
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- Kept Scout history within the Advisor's remaining context budget, including zero-budget behavior, and kept pending Advisor drafts and attachment paths outside Scout's input.
|
|
16
|
+
- Included the current repeated tool invocation in automatic-gate Scout context.
|
|
17
|
+
|
|
18
|
+
## 0.3.1
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
|
|
22
|
+
- Updated the supported runtime baseline to Pi 0.84.1 and verified Herdr reporting against Herdr 0.8.0 while retaining the same request compatibility with Herdr 0.7.5.
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
|
|
26
|
+
- Show configured models and thinking levels first and ticked in `/advisor-models`, allowing Enter to keep each current selection.
|
|
27
|
+
|
|
7
28
|
## 0.3.0
|
|
8
29
|
|
|
9
30
|
### Added
|
package/README.md
CHANGED
|
@@ -20,10 +20,11 @@ The idea is simple: keep implementation on a fast model and borrow frontier reas
|
|
|
20
20
|
- **Separate model and reasoning controls** for the Executor and Advisor.
|
|
21
21
|
- **Privacy controls** for conversation history, repository context, explicit tracked/untracked file handoff, tool results, secret redaction, and outcome logging.
|
|
22
22
|
- **Optional persistent activation, Simple mode, session summaries, and Herdr integration.**
|
|
23
|
+
- **EXPERIMENTAL Advisor Scout** that uses the configured Executor model to curate conversation evidence before every Advisor call.
|
|
23
24
|
|
|
24
25
|
## Install
|
|
25
26
|
|
|
26
|
-
Current release: **0.3.
|
|
27
|
+
Current release: **0.3.2**. Requires Pi 0.84.1 or later and is compatible with Herdr 0.8.0. The extension installs no dependencies of its own; Pi supplies its runtime modules.
|
|
27
28
|
|
|
28
29
|
```bash
|
|
29
30
|
# npm
|
|
@@ -41,7 +42,7 @@ Restart or reload Pi after installation.
|
|
|
41
42
|
## Quick start
|
|
42
43
|
|
|
43
44
|
1. Run `/advisor` to enable the flow and register `ask_advisor`.
|
|
44
|
-
2. Run `/advisor-models` to choose the Executor and Advisor models.
|
|
45
|
+
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.
|
|
45
46
|
3. Run `/advisor-settings` to configure review gates, context, privacy, and limits.
|
|
46
47
|
|
|
47
48
|
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,20 +57,33 @@ You can also enable the flow and select both models at once:
|
|
|
56
57
|
|
|
57
58
|
1. The Executor investigates the task and forms its own candidate direction.
|
|
58
59
|
2. For a consequential decision, stalled attempt, or final review, it calls `ask_advisor` with the reconstructed conversation and allowed repository context.
|
|
59
|
-
3.
|
|
60
|
-
4. The
|
|
60
|
+
3. When Experimental Advisor Scout is enabled, the configured Executor model selects relevant conversation groups and writes a short, explicitly untrusted synthesis.
|
|
61
|
+
4. The Advisor receives selected verbatim evidence, required current-request context, and the unchanged deterministic repository, preference, draft, and attachment regions.
|
|
62
|
+
5. The Executor decides what to adopt, performs the work, and validates the result.
|
|
61
63
|
|
|
62
64
|
A normal consultation never blocks execution. The optional automatic loop gate is different: it evaluates repeated tool calls and applies the configured failure policy when the Advisor says to revise, reports a block, is unavailable, or returns an invalid decision.
|
|
63
65
|
|
|
64
66
|
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.
|
|
65
67
|
|
|
68
|
+
### Experimental Advisor Scout
|
|
69
|
+
|
|
70
|
+
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`.
|
|
71
|
+
|
|
72
|
+
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. The compact result shows the model, selection counts, and elapsed time; `Ctrl+O` shows bounded selected labels and the synthesis.
|
|
73
|
+
|
|
74
|
+
Scout receives a bounded manifest of conversation and tool-history groups after the normal tool disclosure, result-cap, and redaction policies are applied. The manifest and reconstructed Scout conversation share the Advisor's remaining context budget after repository context; 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.
|
|
75
|
+
|
|
76
|
+
A Scout timeout, provider error, missing model or authentication, or invalid response produces a visible fallback. The Advisor then receives the original uncurated conversation. Cancelling the parent operation stops both Scout and Advisor work and does not start fallback. Scout usage is displayed separately and does not spend an Advisor call from the session budget.
|
|
77
|
+
|
|
78
|
+
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. The paper describes an on-demand repository explorer with read, glob, and grep tools. pi-advisor Scout curates conversation history only, and the paper's reported effect sizes do not apply to this feature.
|
|
79
|
+
|
|
66
80
|
## Commands
|
|
67
81
|
|
|
68
82
|
| Command | Purpose |
|
|
69
83
|
| --- | --- |
|
|
70
84
|
| `/advisor` | Enable the flow and optionally override the Executor, Advisor, or context limit. |
|
|
71
85
|
| `/advisor-manual [focus]` | Start a parallel consultation without interrupting the current Executor turn. |
|
|
72
|
-
| `/advisor-models` | Choose both models and their reasoning effort. |
|
|
86
|
+
| `/advisor-models` | Choose both models and their reasoning effort; current models are preselected. |
|
|
73
87
|
| `/advisor-settings` | Configure behavior, context, gates, privacy, and output limits. |
|
|
74
88
|
| `/advisor-off` | Disable the flow and turn off persistent activation. |
|
|
75
89
|
|
|
@@ -79,6 +93,8 @@ The Executor calls `ask_advisor({})` for a general review. It can pass a targete
|
|
|
79
93
|
|
|
80
94
|
Advisor context can include user messages, tool calls, tool results, and repository information. Secret redaction is off by default, and tools without an explicit disclosure policy default to full context. Review the privacy settings before using the extension with sensitive work.
|
|
81
95
|
|
|
96
|
+
When Experimental Advisor Scout is enabled, the Executor model provider also receives bounded Advisor-eligible conversation history. Scout does not receive the deterministic repository, draft, preference, or explicit-file regions described below.
|
|
97
|
+
|
|
82
98
|
Repository context is configurable from no access through changed-file summaries to a capped patch. When context is disabled or its budget is zero, the Advisor is told it was withheld rather than shown an apparently clean tree. Explicit tracked and untracked file contents require separate global opt-ins; attachments are capped, redacted when configured, and sent as untrusted data.
|
|
83
99
|
|
|
84
100
|
## Documentation
|
package/extensions/index.ts
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
parseAutomaticDecision as parseAutomaticDecisionImplementation,
|
|
8
8
|
registerAdvisorTool,
|
|
9
9
|
runAdvisorGate as runAdvisorGateImplementation,
|
|
10
|
+
ScoutStatusManager,
|
|
10
11
|
} from "../src/tools.js";
|
|
11
12
|
|
|
12
13
|
export type { AdvisorConfig, GateFailureMode } from "../src/config.js";
|
|
@@ -31,9 +32,10 @@ export const runAdvisorGate = (
|
|
|
31
32
|
|
|
32
33
|
export default function (pi: ExtensionAPI) {
|
|
33
34
|
const sessionState = new AdvisorSessionState();
|
|
35
|
+
const scoutStatus = new ScoutStatusManager();
|
|
34
36
|
setHerdrBlockedEmitter((active, label) =>
|
|
35
37
|
pi.events.emit("herdr:blocked", { active, label })
|
|
36
38
|
);
|
|
37
|
-
registerAdvisorTool(pi, sessionState);
|
|
38
|
-
registerCommands(pi, { sessionState });
|
|
39
|
+
registerAdvisorTool(pi, sessionState, { statusManager: scoutStatus });
|
|
40
|
+
registerCommands(pi, { sessionState, statusManager: scoutStatus });
|
|
39
41
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-advisor-flow",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.3",
|
|
4
4
|
"author": "Philip Brembeck",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -9,9 +9,9 @@
|
|
|
9
9
|
"main": "extensions/index.ts",
|
|
10
10
|
"devDependencies": {
|
|
11
11
|
"@biomejs/biome": "2.5.3",
|
|
12
|
-
"@earendil-works/pi-ai": "^0.
|
|
13
|
-
"@earendil-works/pi-coding-agent": "^0.
|
|
14
|
-
"@earendil-works/pi-tui": "^0.
|
|
12
|
+
"@earendil-works/pi-ai": "^0.84.1",
|
|
13
|
+
"@earendil-works/pi-coding-agent": "^0.84.1",
|
|
14
|
+
"@earendil-works/pi-tui": "^0.84.1",
|
|
15
15
|
"@types/node": "^20.11.0",
|
|
16
16
|
"bun-types": "^1.0.0",
|
|
17
17
|
"husky": "^9.1.7",
|
|
@@ -21,9 +21,9 @@
|
|
|
21
21
|
"ultracite": "7.9.4"
|
|
22
22
|
},
|
|
23
23
|
"peerDependencies": {
|
|
24
|
-
"@earendil-works/pi-ai": "^0.
|
|
25
|
-
"@earendil-works/pi-coding-agent": "^0.
|
|
26
|
-
"@earendil-works/pi-tui": "^0.
|
|
24
|
+
"@earendil-works/pi-ai": "^0.84.1",
|
|
25
|
+
"@earendil-works/pi-coding-agent": "^0.84.1",
|
|
26
|
+
"@earendil-works/pi-tui": "^0.84.1",
|
|
27
27
|
"typebox": "^1.1.38"
|
|
28
28
|
},
|
|
29
29
|
"peerDependenciesMeta": {
|
package/src/commands.ts
CHANGED
|
@@ -35,6 +35,7 @@ import {
|
|
|
35
35
|
setAdvisorPlanGateRef,
|
|
36
36
|
setAdvisorRedactSecretsRef,
|
|
37
37
|
setAdvisorRef,
|
|
38
|
+
setAdvisorScoutEnabledRef,
|
|
38
39
|
setAdvisorSessionSummaryRef,
|
|
39
40
|
setAdvisorToolPoliciesRef,
|
|
40
41
|
setAdvisorToolResultMaxBytesRef,
|
|
@@ -49,15 +50,18 @@ import {
|
|
|
49
50
|
splitRef,
|
|
50
51
|
} from "./config.js";
|
|
51
52
|
import { herdrAdvisorActivity, notifyHerdrAdvisorFailure } from "./herdr.js";
|
|
53
|
+
import type { ScoutLifecycleEvent } from "./scout.js";
|
|
52
54
|
import type { AdvisorSessionState } from "./session-state.js";
|
|
53
55
|
import {
|
|
54
56
|
adviceForDisplay,
|
|
57
|
+
appendScoutLifecycleEntry,
|
|
55
58
|
consultAdvisor,
|
|
56
59
|
advisorSessionState as defaultAdvisorSessionState,
|
|
57
60
|
hasSoundVerdict,
|
|
58
61
|
renderAdvisorCallBox,
|
|
59
62
|
renderAdvisorResponseHeader,
|
|
60
63
|
resolveAdvisorRequest,
|
|
64
|
+
ScoutStatusManager,
|
|
61
65
|
} from "./tools.js";
|
|
62
66
|
import {
|
|
63
67
|
type AdvisorSettings,
|
|
@@ -66,8 +70,10 @@ import {
|
|
|
66
70
|
SearchableModelSelector,
|
|
67
71
|
} from "./ui.js";
|
|
68
72
|
|
|
73
|
+
const DEFAULT_EFFORT_LEVEL = "Default (Model Default)";
|
|
74
|
+
const SELECTED_PREFIX = "✓ ";
|
|
69
75
|
const EFFORT_LEVELS = [
|
|
70
|
-
|
|
76
|
+
DEFAULT_EFFORT_LEVEL,
|
|
71
77
|
"off",
|
|
72
78
|
"minimal",
|
|
73
79
|
"low",
|
|
@@ -77,6 +83,21 @@ const EFFORT_LEVELS = [
|
|
|
77
83
|
"max",
|
|
78
84
|
];
|
|
79
85
|
|
|
86
|
+
const effortChoices = (configured: string | undefined): string[] => {
|
|
87
|
+
const current = configured ?? DEFAULT_EFFORT_LEVEL;
|
|
88
|
+
return [
|
|
89
|
+
`${SELECTED_PREFIX}${current}`,
|
|
90
|
+
...EFFORT_LEVELS.filter((level) => level !== current),
|
|
91
|
+
];
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const selectedEffort = (choice: string): string | undefined => {
|
|
95
|
+
const effort = choice.startsWith(SELECTED_PREFIX)
|
|
96
|
+
? choice.slice(SELECTED_PREFIX.length)
|
|
97
|
+
: choice;
|
|
98
|
+
return effort === DEFAULT_EFFORT_LEVEL ? undefined : effort;
|
|
99
|
+
};
|
|
100
|
+
|
|
80
101
|
const CONTEXT_PRESETS: ContextPreset[] = [
|
|
81
102
|
{
|
|
82
103
|
description:
|
|
@@ -115,7 +136,9 @@ const CONTEXT_PRESETS: ContextPreset[] = [
|
|
|
115
136
|
type ManualConsult = (
|
|
116
137
|
ctx: ExtensionContext,
|
|
117
138
|
question?: string,
|
|
118
|
-
signal?: AbortSignal
|
|
139
|
+
signal?: AbortSignal,
|
|
140
|
+
onChunk?: (thinking: string, text: string) => void,
|
|
141
|
+
onScout?: (event: ScoutLifecycleEvent) => void
|
|
119
142
|
) => Promise<{
|
|
120
143
|
markdown: string;
|
|
121
144
|
thinkingText: string;
|
|
@@ -144,24 +167,50 @@ export const registerCommands = (
|
|
|
144
167
|
dependencies: {
|
|
145
168
|
consult?: ManualConsult;
|
|
146
169
|
sessionState?: AdvisorSessionState;
|
|
170
|
+
statusManager?: ScoutStatusManager;
|
|
147
171
|
} = {}
|
|
148
172
|
) => {
|
|
149
173
|
const advisorSessionState =
|
|
150
174
|
dependencies.sessionState ?? defaultAdvisorSessionState;
|
|
175
|
+
const scoutStatus = dependencies.statusManager ?? new ScoutStatusManager();
|
|
151
176
|
const flowEnabled = () => pi.getActiveTools().includes("ask_advisor");
|
|
152
177
|
const requestAdvisor =
|
|
153
178
|
dependencies.consult ??
|
|
154
|
-
((ctx, question, signal) =>
|
|
155
|
-
consultAdvisor(
|
|
156
|
-
|
|
179
|
+
((ctx, question, signal, onChunk, onScout) =>
|
|
180
|
+
consultAdvisor(
|
|
181
|
+
ctx,
|
|
182
|
+
question,
|
|
183
|
+
signal,
|
|
184
|
+
onChunk,
|
|
185
|
+
"manual",
|
|
186
|
+
undefined,
|
|
187
|
+
undefined,
|
|
188
|
+
undefined,
|
|
189
|
+
undefined,
|
|
190
|
+
onScout
|
|
191
|
+
));
|
|
192
|
+
const manualConsultations = new Map<AbortController, symbol>();
|
|
157
193
|
|
|
158
194
|
const startManualConsultation = (
|
|
159
195
|
ctx: ExtensionContext,
|
|
160
196
|
question: string | undefined,
|
|
161
|
-
controller: AbortController
|
|
197
|
+
controller: AbortController,
|
|
198
|
+
scoutStatusToken: symbol
|
|
162
199
|
) => {
|
|
163
200
|
herdrAdvisorActivity.start();
|
|
164
|
-
|
|
201
|
+
let scoutDetails: Parameters<typeof appendScoutLifecycleEntry>[2];
|
|
202
|
+
return requestAdvisor(
|
|
203
|
+
ctx,
|
|
204
|
+
question,
|
|
205
|
+
controller.signal,
|
|
206
|
+
undefined,
|
|
207
|
+
(event) => {
|
|
208
|
+
if (!controller.signal.aborted) {
|
|
209
|
+
scoutStatus.update(ctx, scoutStatusToken, event);
|
|
210
|
+
scoutDetails = appendScoutLifecycleEntry(pi, event, scoutDetails);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
)
|
|
165
214
|
.then(({ markdown }) => {
|
|
166
215
|
if (controller.signal.aborted) {
|
|
167
216
|
return;
|
|
@@ -215,6 +264,7 @@ export const registerCommands = (
|
|
|
215
264
|
notifyHerdrAdvisorFailure("Advisor consultation failed", message);
|
|
216
265
|
})
|
|
217
266
|
.finally(() => {
|
|
267
|
+
scoutStatus.release(ctx, scoutStatusToken);
|
|
218
268
|
manualConsultations.delete(controller);
|
|
219
269
|
herdrAdvisorActivity.finish();
|
|
220
270
|
});
|
|
@@ -381,10 +431,12 @@ export const registerCommands = (
|
|
|
381
431
|
saveConfig(ctx);
|
|
382
432
|
});
|
|
383
433
|
|
|
384
|
-
pi.on("session_shutdown", () => {
|
|
385
|
-
for (const controller of manualConsultations) {
|
|
434
|
+
pi.on("session_shutdown", (_event, ctx) => {
|
|
435
|
+
for (const [controller, token] of manualConsultations) {
|
|
386
436
|
controller.abort();
|
|
437
|
+
scoutStatus.release(ctx, token);
|
|
387
438
|
}
|
|
439
|
+
scoutStatus.clear(ctx);
|
|
388
440
|
manualConsultations.clear();
|
|
389
441
|
herdrAdvisorActivity.clear();
|
|
390
442
|
});
|
|
@@ -413,14 +465,17 @@ export const registerCommands = (
|
|
|
413
465
|
const question = resolveAdvisorRequest(args);
|
|
414
466
|
// A single visible progress surface avoids competing consultations overwriting
|
|
415
467
|
// each other's streamed state. A newer manual request replaces the previous one.
|
|
416
|
-
for (const pending of manualConsultations) {
|
|
468
|
+
for (const [pending, token] of manualConsultations) {
|
|
417
469
|
pending.abort();
|
|
470
|
+
scoutStatus.release(ctx, token);
|
|
418
471
|
}
|
|
419
472
|
manualConsultations.clear();
|
|
420
473
|
const controller = new AbortController();
|
|
421
|
-
|
|
474
|
+
const scoutStatusToken = Symbol("manual-scout");
|
|
475
|
+
scoutStatus.register(scoutStatusToken);
|
|
476
|
+
manualConsultations.set(controller, scoutStatusToken);
|
|
422
477
|
pi.appendEntry?.("advisor-manual-call", { question });
|
|
423
|
-
startManualConsultation(ctx, question, controller);
|
|
478
|
+
startManualConsultation(ctx, question, controller, scoutStatusToken);
|
|
424
479
|
return Promise.resolve();
|
|
425
480
|
},
|
|
426
481
|
});
|
|
@@ -446,6 +501,7 @@ export const registerCommands = (
|
|
|
446
501
|
(tui, theme, keybindings, done) =>
|
|
447
502
|
new SearchableModelSelector({
|
|
448
503
|
allOptions: refs,
|
|
504
|
+
currentOption: executorRef,
|
|
449
505
|
keybindings,
|
|
450
506
|
onCancel: () => done(undefined),
|
|
451
507
|
onSelect: done,
|
|
@@ -460,7 +516,7 @@ export const registerCommands = (
|
|
|
460
516
|
|
|
461
517
|
const executorEffort = await ctx.ui.select(
|
|
462
518
|
"Select Executor Reasoning/Thinking Level",
|
|
463
|
-
|
|
519
|
+
effortChoices(executorEffortRef)
|
|
464
520
|
);
|
|
465
521
|
if (!executorEffort) {
|
|
466
522
|
return;
|
|
@@ -470,6 +526,7 @@ export const registerCommands = (
|
|
|
470
526
|
(tui, theme, keybindings, done) =>
|
|
471
527
|
new SearchableModelSelector({
|
|
472
528
|
allOptions: refs,
|
|
529
|
+
currentOption: advisorRef,
|
|
473
530
|
keybindings,
|
|
474
531
|
onCancel: () => done(undefined),
|
|
475
532
|
onSelect: done,
|
|
@@ -484,7 +541,7 @@ export const registerCommands = (
|
|
|
484
541
|
|
|
485
542
|
const advisorEffort = await ctx.ui.select(
|
|
486
543
|
"Select Advisor Reasoning/Thinking Level",
|
|
487
|
-
|
|
544
|
+
effortChoices(advisorEffortRef)
|
|
488
545
|
);
|
|
489
546
|
if (!advisorEffort) {
|
|
490
547
|
return;
|
|
@@ -492,14 +549,8 @@ export const registerCommands = (
|
|
|
492
549
|
|
|
493
550
|
setExecutorRef(executor);
|
|
494
551
|
setAdvisorRef(advisor);
|
|
495
|
-
setExecutorEffortRef(
|
|
496
|
-
|
|
497
|
-
? undefined
|
|
498
|
-
: executorEffort
|
|
499
|
-
);
|
|
500
|
-
setAdvisorEffortRef(
|
|
501
|
-
advisorEffort === "Default (Model Default)" ? undefined : advisorEffort
|
|
502
|
-
);
|
|
552
|
+
setExecutorEffortRef(selectedEffort(executorEffort));
|
|
553
|
+
setAdvisorEffortRef(selectedEffort(advisorEffort));
|
|
503
554
|
|
|
504
555
|
const path = saveConfig(ctx);
|
|
505
556
|
ctx.ui.notify(
|
|
@@ -550,6 +601,7 @@ export const registerCommands = (
|
|
|
550
601
|
setAdvisorLoopThresholdRef(settings.loopThreshold ?? 3);
|
|
551
602
|
setAdvisorMaxCallsPerSessionRef(settings.maxCallsPerSession);
|
|
552
603
|
setAdvisorSessionSummaryRef(settings.sessionSummary ?? false);
|
|
604
|
+
setAdvisorScoutEnabledRef(settings.scoutEnabled ?? false);
|
|
553
605
|
setSimpleModeRef(settings.simpleMode ?? false);
|
|
554
606
|
setAlwaysOnRef(settings.alwaysOn ?? false);
|
|
555
607
|
setAdvisorFailureModeRef(settings.failureMode ?? "block-session");
|
package/src/config.ts
CHANGED
|
@@ -65,6 +65,7 @@ export let advisorToolPoliciesRef: AdvisorToolPolicies = {};
|
|
|
65
65
|
export let advisorOutcomeLoggingRef = false;
|
|
66
66
|
export let advisorUntrackedContentRef = false;
|
|
67
67
|
export let advisorTrackedFileContentRef = false;
|
|
68
|
+
export let advisorScoutEnabledRef = false;
|
|
68
69
|
|
|
69
70
|
export const setExecutorRef = (ref: string) => {
|
|
70
71
|
executorRef = ref;
|
|
@@ -170,6 +171,9 @@ export const setAdvisorUntrackedContentRef = (enabled: boolean) => {
|
|
|
170
171
|
export const setAdvisorTrackedFileContentRef = (enabled: boolean) => {
|
|
171
172
|
advisorTrackedFileContentRef = enabled;
|
|
172
173
|
};
|
|
174
|
+
export const setAdvisorScoutEnabledRef = (enabled: boolean) => {
|
|
175
|
+
advisorScoutEnabledRef = enabled;
|
|
176
|
+
};
|
|
173
177
|
|
|
174
178
|
/**
|
|
175
179
|
* Returns the current live settings state. Use this at UI boundaries instead of
|
|
@@ -194,6 +198,7 @@ export const getAdvisorSettings = () => ({
|
|
|
194
198
|
outcomeLogging: advisorOutcomeLoggingRef,
|
|
195
199
|
planGate: advisorPlanGateRef,
|
|
196
200
|
redactSecrets: advisorRedactSecretsRef,
|
|
201
|
+
scoutEnabled: advisorScoutEnabledRef,
|
|
197
202
|
sessionSummary: advisorSessionSummaryRef,
|
|
198
203
|
simpleMode: simpleModeRef,
|
|
199
204
|
toolPolicies: { ...advisorToolPoliciesRef },
|
|
@@ -232,6 +237,7 @@ export interface AdvisorConfig {
|
|
|
232
237
|
advisorOutcomeLogging?: boolean;
|
|
233
238
|
advisorPlanGate?: boolean;
|
|
234
239
|
advisorRedactSecrets?: boolean;
|
|
240
|
+
advisorScoutEnabled?: boolean;
|
|
235
241
|
advisorSessionSummary?: boolean;
|
|
236
242
|
advisorToolPolicies?: AdvisorToolPolicies;
|
|
237
243
|
advisorToolResultMaxBytes?: number;
|
|
@@ -262,6 +268,7 @@ const CONFIG_KEYS = new Set<keyof AdvisorConfig>([
|
|
|
262
268
|
"advisorMaxCallsPerSession",
|
|
263
269
|
"advisorPlanGate",
|
|
264
270
|
"advisorSessionSummary",
|
|
271
|
+
"advisorScoutEnabled",
|
|
265
272
|
"simpleMode",
|
|
266
273
|
"alwaysOn",
|
|
267
274
|
"advisorToolResultMaxBytes",
|
|
@@ -284,6 +291,7 @@ const BOOLEAN_CONFIG_KEYS = [
|
|
|
284
291
|
"advisorBlockOnBlocked",
|
|
285
292
|
"advisorAutoLoopGate",
|
|
286
293
|
"advisorSessionSummary",
|
|
294
|
+
"advisorScoutEnabled",
|
|
287
295
|
"simpleMode",
|
|
288
296
|
"alwaysOn",
|
|
289
297
|
"advisorHerdrIntegration",
|
|
@@ -469,6 +477,7 @@ const resetDefaults = () => {
|
|
|
469
477
|
advisorOutcomeLoggingRef = false;
|
|
470
478
|
advisorUntrackedContentRef = false;
|
|
471
479
|
advisorTrackedFileContentRef = false;
|
|
480
|
+
advisorScoutEnabledRef = false;
|
|
472
481
|
};
|
|
473
482
|
|
|
474
483
|
const applyOptionalConfig = <Key extends keyof AdvisorConfig>(
|
|
@@ -535,6 +544,7 @@ const applyConfig = (config: AdvisorConfig) => {
|
|
|
535
544
|
"advisorSessionSummary",
|
|
536
545
|
setAdvisorSessionSummaryRef
|
|
537
546
|
);
|
|
547
|
+
applyOptionalConfig(config, "advisorScoutEnabled", setAdvisorScoutEnabledRef);
|
|
538
548
|
applyOptionalConfig(config, "simpleMode", setSimpleModeRef);
|
|
539
549
|
applyOptionalConfig(config, "alwaysOn", setAlwaysOnRef);
|
|
540
550
|
applyOptionalConfig(config, "gateFailureMode", setAdvisorFailureModeRef);
|
|
@@ -693,6 +703,7 @@ export const saveConfig = (_ctx: ExtensionContext) => {
|
|
|
693
703
|
advisorGitContextMaxChars: advisorGitContextMaxCharsRef,
|
|
694
704
|
advisorHerdrIntegration: advisorHerdrIntegrationRef,
|
|
695
705
|
advisorRedactSecrets: advisorRedactSecretsRef,
|
|
706
|
+
advisorScoutEnabled: advisorScoutEnabledRef,
|
|
696
707
|
advisorSessionSummary: advisorSessionSummaryRef,
|
|
697
708
|
advisorToolPolicies: advisorToolPoliciesRef,
|
|
698
709
|
advisorToolResultMaxBytes: advisorToolResultMaxBytesRef,
|
package/src/conversation.ts
CHANGED
|
@@ -247,7 +247,7 @@ const toolResultEntry = (
|
|
|
247
247
|
return `[Tool Result for ${toolName}] (${message.isError ? "Error " : ""}output):\n${capped.content}`;
|
|
248
248
|
};
|
|
249
249
|
|
|
250
|
-
const conversationEntry = (
|
|
250
|
+
export const conversationEntry = (
|
|
251
251
|
entry: unknown,
|
|
252
252
|
toolResultMaxLines: number,
|
|
253
253
|
toolResultMaxBytes: number,
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type Api,
|
|
3
|
+
type AssistantMessage,
|
|
4
|
+
type Message,
|
|
5
|
+
type Model,
|
|
6
|
+
stream,
|
|
7
|
+
} from "@earendil-works/pi-ai/compat";
|
|
8
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
9
|
+
import { splitRef } from "./config.js";
|
|
10
|
+
|
|
11
|
+
export interface ResolvedConfiguredModel {
|
|
12
|
+
apiKey: string;
|
|
13
|
+
env?: Record<string, string>;
|
|
14
|
+
headers?: Record<string, string | null>;
|
|
15
|
+
model: Model<Api>;
|
|
16
|
+
ref: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const resolveConfiguredModel = async (
|
|
20
|
+
ctx: ExtensionContext,
|
|
21
|
+
ref: string,
|
|
22
|
+
label: string
|
|
23
|
+
): Promise<ResolvedConfiguredModel> => {
|
|
24
|
+
const [provider, modelId] = splitRef(ref);
|
|
25
|
+
const model = ctx.modelRegistry.find(provider, modelId);
|
|
26
|
+
if (!model) {
|
|
27
|
+
throw new Error(`${label} model not found: ${ref}`);
|
|
28
|
+
}
|
|
29
|
+
const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
|
|
30
|
+
if (!auth.ok) {
|
|
31
|
+
throw new Error((auth as { error: string }).error);
|
|
32
|
+
}
|
|
33
|
+
if (!auth.apiKey) {
|
|
34
|
+
throw new Error(`No API key for ${ref}`);
|
|
35
|
+
}
|
|
36
|
+
return {
|
|
37
|
+
apiKey: auth.apiKey,
|
|
38
|
+
env: auth.env,
|
|
39
|
+
headers: auth.headers,
|
|
40
|
+
model,
|
|
41
|
+
ref,
|
|
42
|
+
};
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export interface CollectTextStreamOptions {
|
|
46
|
+
messages: Message[];
|
|
47
|
+
onChunk?: (thinking: string, text: string) => void;
|
|
48
|
+
reasoning?: string;
|
|
49
|
+
signal?: AbortSignal;
|
|
50
|
+
systemPrompt: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface CollectedTextStream {
|
|
54
|
+
text: string;
|
|
55
|
+
thinking: string;
|
|
56
|
+
usage?: unknown;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export const collectTextStream = async (
|
|
60
|
+
resolved: ResolvedConfiguredModel,
|
|
61
|
+
options: CollectTextStreamOptions,
|
|
62
|
+
streamModel: typeof stream = stream
|
|
63
|
+
): Promise<CollectedTextStream> => {
|
|
64
|
+
let thinking = "";
|
|
65
|
+
let text = "";
|
|
66
|
+
const eventStream = streamModel(
|
|
67
|
+
resolved.model,
|
|
68
|
+
{ messages: options.messages, systemPrompt: options.systemPrompt },
|
|
69
|
+
{
|
|
70
|
+
apiKey: resolved.apiKey,
|
|
71
|
+
env: resolved.env,
|
|
72
|
+
headers: resolved.headers,
|
|
73
|
+
reasoning: options.reasoning as never,
|
|
74
|
+
signal: options.signal,
|
|
75
|
+
}
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
for await (const event of eventStream) {
|
|
79
|
+
if (event.type === "thinking_delta") {
|
|
80
|
+
thinking += event.delta;
|
|
81
|
+
options.onChunk?.(thinking, text);
|
|
82
|
+
} else if (event.type === "text_delta") {
|
|
83
|
+
text += event.delta;
|
|
84
|
+
options.onChunk?.(thinking, text);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const response = await eventStream.result();
|
|
89
|
+
const lastAssistant = [response].find(
|
|
90
|
+
(message): message is AssistantMessage => message.role === "assistant"
|
|
91
|
+
);
|
|
92
|
+
const finalText =
|
|
93
|
+
lastAssistant?.content
|
|
94
|
+
.filter(
|
|
95
|
+
(part): part is { type: "text"; text: string } => part.type === "text"
|
|
96
|
+
)
|
|
97
|
+
.map((part) => part.text)
|
|
98
|
+
.join("\n") || text;
|
|
99
|
+
return {
|
|
100
|
+
text: finalText,
|
|
101
|
+
thinking,
|
|
102
|
+
usage: (
|
|
103
|
+
lastAssistant as (AssistantMessage & { usage?: unknown }) | undefined
|
|
104
|
+
)?.usage,
|
|
105
|
+
};
|
|
106
|
+
};
|