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 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.0**. Requires Pi 0.80.7 or later. The extension installs no dependencies of its own; Pi supplies its runtime modules.
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. The Advisor returns a concise review. It may challenge assumptions, identify risks, or recommend the next verification step.
60
- 4. The Executor decides what to adopt, performs the work, and validates the result.
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
@@ -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.0",
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.80.7",
13
- "@earendil-works/pi-coding-agent": "^0.80.7",
14
- "@earendil-works/pi-tui": "^0.80.7",
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.80.7",
25
- "@earendil-works/pi-coding-agent": "^0.80.7",
26
- "@earendil-works/pi-tui": "^0.80.7",
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
- "Default (Model Default)",
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(ctx, question, signal, undefined, "manual"));
156
- const manualConsultations = new Set<AbortController>();
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
- return requestAdvisor(ctx, question, controller.signal)
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
- manualConsultations.add(controller);
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
- EFFORT_LEVELS
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
- EFFORT_LEVELS
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
- executorEffort === "Default (Model Default)"
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,
@@ -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
+ };