pi-advisor-flow 0.5.1 → 0.5.2

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.
Files changed (59) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +3 -2
  3. package/extensions/index.ts +7 -7
  4. package/package.json +2 -1
  5. package/src/commands/activation-preparation.ts +117 -0
  6. package/src/commands/activation.ts +135 -0
  7. package/src/commands/lifecycle.ts +83 -0
  8. package/src/commands/manual-command.ts +107 -0
  9. package/src/commands/manual-consultation.ts +144 -0
  10. package/src/commands/manual-progress.ts +93 -0
  11. package/src/commands/model-commands.ts +66 -0
  12. package/src/commands/model-options.ts +181 -0
  13. package/src/commands/model-picker.ts +110 -0
  14. package/src/commands/registration.ts +27 -0
  15. package/src/commands/renderers.ts +76 -0
  16. package/src/commands/runtime.ts +114 -0
  17. package/src/commands/settings-commands.ts +80 -0
  18. package/src/commands/settings-persistence.ts +81 -0
  19. package/src/commands/types.ts +78 -0
  20. package/src/commands.ts +9 -1259
  21. package/src/config/args.ts +37 -0
  22. package/src/config/defaults.ts +190 -0
  23. package/src/config/state.ts +199 -0
  24. package/src/config/storage.ts +224 -0
  25. package/src/config/types.ts +63 -0
  26. package/src/config/validation.ts +234 -0
  27. package/src/config.ts +104 -814
  28. package/src/conversation.ts +4 -2
  29. package/src/herdr.ts +1 -1
  30. package/src/model-stream.ts +15 -1
  31. package/src/scout-context.ts +2 -2
  32. package/src/scout.ts +1 -1
  33. package/src/tools/consultation.ts +337 -0
  34. package/src/tools/gate-policy.ts +132 -0
  35. package/src/tools/gate-protocol.ts +127 -0
  36. package/src/tools/loop-gate.ts +210 -0
  37. package/src/tools/prompts.ts +159 -0
  38. package/src/tools/register-ask-advisor.ts +225 -0
  39. package/src/tools/register-lifecycle.ts +89 -0
  40. package/src/tools/register-outcome.ts +75 -0
  41. package/src/tools/register-renderers.ts +88 -0
  42. package/src/tools/registration.ts +35 -0
  43. package/src/tools/render-advisor-result.ts +168 -0
  44. package/src/tools/render-common.ts +120 -0
  45. package/src/tools/scout-status.ts +206 -0
  46. package/src/tools/session.ts +3 -0
  47. package/src/tools/types.ts +119 -0
  48. package/src/tools.ts +42 -1865
  49. package/src/ui/manual-dialog-render.ts +150 -0
  50. package/src/ui/manual-dialog.ts +305 -0
  51. package/src/ui/model-selector.ts +166 -0
  52. package/src/ui/settings-formatting.ts +133 -0
  53. package/src/ui/settings-items.ts +323 -0
  54. package/src/ui/settings-list-adapter.ts +103 -0
  55. package/src/ui/settings-mutations.ts +80 -0
  56. package/src/ui/settings-selector.ts +161 -0
  57. package/src/ui/text-setting-submenu.ts +66 -0
  58. package/src/ui/types.ts +111 -0
  59. package/src/ui.ts +9 -1418
package/CHANGELOG.md CHANGED
@@ -4,6 +4,20 @@ 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.2 - 2026-09-05
8
+
9
+ ### Changed
10
+
11
+ - Reorganized configuration, UI, Advisor tool, and command code into smaller modules behind the existing public facades without changing the extension's public API.
12
+
13
+ ### Fixed
14
+
15
+ - Kept failed or aborted Advisor streams from treating partial responses as successful advice or automatic gate permission.
16
+ - Made malformed automatic-gate Markdown fences fail closed when their character or delimiter length does not match.
17
+ - Applied optional secret redaction to targeted Advisor questions before sending provider requests while retaining the raw question for local displays.
18
+ - Kept the TUI tab keybinding out of Socket's URL-string heuristic without changing its runtime behavior.
19
+ - Made benchmark extension attestations follow the root `package.json` version instead of a duplicated constant.
20
+
7
21
  ## 0.5.1
8
22
 
9
23
  ### Changed
package/README.md CHANGED
@@ -8,7 +8,8 @@ A configurable second-opinion workflow for <a href="https://github.com/earendil-
8
8
 
9
9
  </div>
10
10
 
11
- ![d18m Downloads](https://img.shields.io/npm/d18m/pi-advisor-flow?style=flat) ![NPM Version](https://img.shields.io/npm/v/pi-advisor-flow?style=flat)
11
+ ![d18m Downloads](https://img.shields.io/npm/d18m/pi-advisor-flow?style=flat) ![NPM Version](https://img.shields.io/npm/v/pi-advisor-flow?style=flat) ![Pi Advisor Flow badge](https://img.shields.io/badge/advisor%20flow-fff?logo=pi&logoColor=000)
12
+
12
13
 
13
14
  `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.
14
15
 
@@ -70,7 +71,7 @@ Advisor Scout is off by default. When enabled, the Executor model first selects
70
71
 
71
72
  ## Privacy
72
73
 
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.
74
+ Advisor requests can include user messages, tool calls, tool results, targeted questions, and repository information. `/advisor-settings` controls context, tool disclosure, redaction, and explicit file handoff. Secret redaction is off by default; when enabled, credential-shaped values in targeted questions are redacted before the provider request. Tools without an explicit policy use full context. Settings are global, so a project cannot silently change them.
74
75
 
75
76
  Read [Privacy and data handling](https://github.com/philipbrembeck/pi-advisor/blob/main/docs/privacy.md) before using pi-advisor with sensitive work.
76
77
 
@@ -1,16 +1,16 @@
1
1
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
- import { registerCommands } from "../src/commands.js";
2
+ import { registerCommands } from "../src/commands/registration.js";
3
3
  import { setHerdrBlockedEmitter } from "../src/herdr.js";
4
4
  import { AdvisorSessionState } from "../src/session-state.js";
5
5
  import {
6
6
  consultAdvisor as consultAdvisorImplementation,
7
- parseAutomaticDecision as parseAutomaticDecisionImplementation,
8
- registerAdvisorTool,
9
7
  runAdvisorGate as runAdvisorGateImplementation,
10
- ScoutStatusManager,
11
- } from "../src/tools.js";
8
+ } from "../src/tools/consultation.js";
9
+ import { parseAutomaticDecision as parseAutomaticDecisionImplementation } from "../src/tools/gate-protocol.js";
10
+ import { registerAdvisorTool } from "../src/tools/registration.js";
11
+ import { ScoutStatusManager } from "../src/tools/scout-status.js";
12
12
 
13
- export type { AdvisorConfig, GateFailureMode } from "../src/config.js";
13
+ export type { AdvisorConfig, GateFailureMode } from "../src/config/types.js";
14
14
  export type {
15
15
  AdvisorConsultationResult,
16
16
  AdvisorGateFailure,
@@ -19,7 +19,7 @@ export type {
19
19
  ConsultationTrigger,
20
20
  GateDecision,
21
21
  GateTrigger,
22
- } from "../src/tools.js";
22
+ } from "../src/tools/types.js";
23
23
  export const consultAdvisor = (
24
24
  ...args: Parameters<typeof consultAdvisorImplementation>
25
25
  ) => consultAdvisorImplementation(...args);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-advisor-flow",
3
- "version": "0.5.1",
3
+ "version": "0.5.2",
4
4
  "description": "Advanced Executor/Advisor flow for Pi, fully configurable and extendable.",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -32,6 +32,7 @@
32
32
  "bench:evaluate": "bun bench/src/cli.ts evaluate",
33
33
  "bench:replay": "bun bench/src/cli.ts replay",
34
34
  "bench:screen": "bun bench/src/cli.ts screen",
35
+ "check:boundaries": "node scripts/check-module-boundaries.mjs",
35
36
  "format": "bunx ultracite fix --linter-enabled=false",
36
37
  "lint": "bunx ultracite check",
37
38
  "lint:fix": "bunx ultracite fix",
@@ -0,0 +1,117 @@
1
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ import {
3
+ advisorEffortRef,
4
+ advisorRef,
5
+ executorEffortRef,
6
+ executorRef,
7
+ getPersistedModelRefs,
8
+ setAdvisorEffortRef,
9
+ setAdvisorRef,
10
+ setExecutorEffortRef,
11
+ setExecutorRef,
12
+ } from "../config/state.js";
13
+ import { loadConfig } from "../config/storage.js";
14
+ import {
15
+ getAvailableModelRefs,
16
+ getExplicitModelError,
17
+ planActivationModels,
18
+ } from "./model-options.js";
19
+ import { selectAdvisorModels } from "./model-picker.js";
20
+ import { notify } from "./runtime.js";
21
+ import type { CommandRuntime } from "./types.js";
22
+
23
+ export interface PreparedActivationModels {
24
+ pendingExecutor?: string;
25
+ pickedModels: boolean;
26
+ }
27
+
28
+ export const loadCommandConfig = (ctx: ExtensionContext) => {
29
+ try {
30
+ loadConfig(ctx);
31
+ return true;
32
+ } catch (error) {
33
+ const message = error instanceof Error ? error.message : String(error);
34
+ notify(
35
+ ctx,
36
+ `Advisor command could not load configuration: ${message} Fix advisor.json and retry.`,
37
+ "error"
38
+ );
39
+ return false;
40
+ }
41
+ };
42
+
43
+ export const prepareActivationModels = async (
44
+ runtime: CommandRuntime,
45
+ ctx: ExtensionContext,
46
+ announce: boolean,
47
+ executorOverride: boolean,
48
+ advisorOverride: boolean
49
+ ): Promise<PreparedActivationModels | undefined> => {
50
+ const persisted = getPersistedModelRefs();
51
+ const availableRefs = getAvailableModelRefs(ctx);
52
+ const availableRefSet = availableRefs ? new Set(availableRefs) : undefined;
53
+ const explicitError =
54
+ getExplicitModelError(
55
+ ctx,
56
+ executorRef,
57
+ "Executor",
58
+ executorOverride,
59
+ availableRefSet
60
+ ) ??
61
+ getExplicitModelError(
62
+ ctx,
63
+ advisorRef,
64
+ "Advisor",
65
+ advisorOverride,
66
+ availableRefSet
67
+ );
68
+ if (explicitError) {
69
+ notify(ctx, explicitError, "error");
70
+ return;
71
+ }
72
+
73
+ const plan = planActivationModels(
74
+ ctx,
75
+ executorRef,
76
+ advisorRef,
77
+ runtime.pendingExecutorModelRef,
78
+ persisted,
79
+ executorOverride,
80
+ advisorOverride,
81
+ availableRefSet
82
+ );
83
+ setExecutorRef(plan.pendingExecutor ?? executorRef);
84
+ if (!(plan.selectExecutor || plan.selectAdvisor)) {
85
+ return { pendingExecutor: plan.pendingExecutor, pickedModels: false };
86
+ }
87
+
88
+ // Always-on startup cannot open an interactive picker, so it leaves the
89
+ // flow disabled until the user selects both models with `/advisor`.
90
+ if (!announce) {
91
+ notify(
92
+ ctx,
93
+ "Advisor models are not configured or available. Run /advisor to choose them.",
94
+ "error"
95
+ );
96
+ return;
97
+ }
98
+ const selection = await selectAdvisorModels(ctx, {
99
+ advisor: advisorOverride || persisted.advisor ? advisorRef : "",
100
+ advisorEffort: advisorEffortRef,
101
+ executor:
102
+ executorOverride || plan.pendingExecutor || persisted.executor
103
+ ? executorRef
104
+ : "",
105
+ executorEffort: executorEffortRef,
106
+ selectAdvisor: plan.selectAdvisor,
107
+ selectExecutor: plan.selectExecutor,
108
+ });
109
+ if (!selection) {
110
+ return;
111
+ }
112
+ setAdvisorRef(selection.advisor);
113
+ setAdvisorEffortRef(selection.advisorEffort);
114
+ setExecutorRef(selection.executor);
115
+ setExecutorEffortRef(selection.executorEffort);
116
+ return { pendingExecutor: plan.pendingExecutor, pickedModels: true };
117
+ };
@@ -0,0 +1,135 @@
1
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ import { parseArgs } from "../config/args.js";
3
+ import {
4
+ advisorEffortRef,
5
+ advisorRef,
6
+ contextMaxCharsRef,
7
+ executorEffortRef,
8
+ executorRef,
9
+ setAdvisorEffortRef,
10
+ setAdvisorRef,
11
+ setContextMaxCharsRef,
12
+ setExecutorEffortRef,
13
+ setExecutorRef,
14
+ } from "../config/state.js";
15
+ import { saveConfig } from "../config/storage.js";
16
+ import {
17
+ loadCommandConfig,
18
+ prepareActivationModels,
19
+ } from "./activation-preparation.js";
20
+ import {
21
+ ADVISOR_ACTIVATION_EXPLANATION,
22
+ findConfiguredModel,
23
+ hasAdvisorOverride,
24
+ hasExecutorOverride,
25
+ } from "./model-options.js";
26
+ import { notify } from "./runtime.js";
27
+ import type { CommandRuntime, ThinkingLevel } from "./types.js";
28
+
29
+ const resolveActivationModels = async (
30
+ runtime: CommandRuntime,
31
+ ctx: ExtensionContext
32
+ ) => {
33
+ const executor = findConfiguredModel(ctx, executorRef);
34
+ if (!executor) {
35
+ return {
36
+ error: executorRef
37
+ ? `Executor model not found: ${executorRef}`
38
+ : "Executor model not configured",
39
+ };
40
+ }
41
+ const advisor = findConfiguredModel(ctx, advisorRef);
42
+ if (!advisor) {
43
+ return {
44
+ error: advisorRef
45
+ ? `Advisor model not found: ${advisorRef}`
46
+ : "Advisor model not configured",
47
+ };
48
+ }
49
+ const advisorAuth = await ctx.modelRegistry.getApiKeyAndHeaders(advisor);
50
+ if (!(advisorAuth.ok && advisorAuth.apiKey)) {
51
+ return { error: `No API key for Advisor ${advisorRef}` };
52
+ }
53
+ if (!(await runtime.setExecutorModel(executor))) {
54
+ return { error: `No API key for Executor ${executorRef}` };
55
+ }
56
+ return {};
57
+ };
58
+
59
+ export const activateAdvisor = async (
60
+ runtime: CommandRuntime,
61
+ args: string,
62
+ ctx: ExtensionContext,
63
+ announce = true
64
+ ) => {
65
+ if (!loadCommandConfig(ctx)) {
66
+ return;
67
+ }
68
+ const previous = {
69
+ advisor: advisorRef,
70
+ advisorEffort: advisorEffortRef,
71
+ contextMaxChars: contextMaxCharsRef,
72
+ executor: executorRef,
73
+ executorEffort: executorEffortRef,
74
+ };
75
+ const restoreRefs = () => {
76
+ setAdvisorRef(previous.advisor);
77
+ setAdvisorEffortRef(previous.advisorEffort);
78
+ setContextMaxCharsRef(previous.contextMaxChars);
79
+ setExecutorRef(previous.executor);
80
+ setExecutorEffortRef(previous.executorEffort);
81
+ };
82
+ const executorOverride = hasExecutorOverride(args);
83
+ const advisorOverride = hasAdvisorOverride(args);
84
+ const argumentError = parseArgs(args);
85
+ if (argumentError) {
86
+ restoreRefs();
87
+ notify(ctx, argumentError, "error");
88
+ return;
89
+ }
90
+
91
+ const prepared = await prepareActivationModels(
92
+ runtime,
93
+ ctx,
94
+ announce,
95
+ executorOverride,
96
+ advisorOverride
97
+ );
98
+ if (!prepared) {
99
+ restoreRefs();
100
+ return;
101
+ }
102
+ const { error } = await resolveActivationModels(runtime, ctx);
103
+ if (error) {
104
+ restoreRefs();
105
+ notify(ctx, error, "error");
106
+ return;
107
+ }
108
+ // parseArgs, model picking, and an inactive `/model` selection only mutate
109
+ // in-memory refs. Persist them once both models are known and authenticated,
110
+ // so an unusable model reference is never written to the configuration.
111
+ if (args.trim() || prepared.pickedModels || prepared.pendingExecutor) {
112
+ saveConfig(ctx, { persistAdvisor: true, persistExecutor: true });
113
+ }
114
+ // A successful activation has committed the effective Executor. Do not let
115
+ // an older inactive selection override an explicit activation argument on a
116
+ // later attempt.
117
+ runtime.pendingExecutorModelRef = undefined;
118
+ if (executorEffortRef) {
119
+ runtime.pi.setThinkingLevel(executorEffortRef as ThinkingLevel);
120
+ }
121
+ if (!runtime.flowEnabled()) {
122
+ runtime.pi.setActiveTools([
123
+ ...runtime.pi.getActiveTools(),
124
+ "ask_advisor",
125
+ "record_advisor_outcome",
126
+ ]);
127
+ }
128
+ if (announce) {
129
+ notify(
130
+ ctx,
131
+ `${ADVISOR_ACTIVATION_EXPLANATION}\n\nAdvisor flow ready — Executor: ${executorRef} (thinking: ${executorEffortRef || "default"}) · Advisor: ${advisorRef} (thinking: ${advisorEffortRef || "default"})`,
132
+ "info"
133
+ );
134
+ }
135
+ };
@@ -0,0 +1,83 @@
1
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ import {
3
+ alwaysOnRef,
4
+ executorRef,
5
+ getPersistedModelRefs,
6
+ setExecutorRef,
7
+ } from "../config/state.js";
8
+ import { loadConfig, saveConfig } from "../config/storage.js";
9
+ import { herdrAdvisorActivity } from "../herdr.js";
10
+ import { notify } from "./runtime.js";
11
+ import type { CommandRuntime } from "./types.js";
12
+
13
+ export type ActivateAdvisor = (
14
+ args: string,
15
+ ctx: ExtensionContext,
16
+ announce?: boolean
17
+ ) => Promise<void>;
18
+
19
+ export const registerCommandLifecycle = (
20
+ runtime: CommandRuntime,
21
+ activateAdvisor: ActivateAdvisor
22
+ ) => {
23
+ runtime.pi.on("session_start", async (_event, ctx) => {
24
+ runtime.pendingExecutorModelRef = undefined;
25
+ // A malformed advisor.json or a provider auth failure must not reject a
26
+ // lifecycle handler and break session startup.
27
+ try {
28
+ loadConfig(ctx);
29
+ if (alwaysOnRef) {
30
+ await activateAdvisor("", ctx, false);
31
+ }
32
+ } catch (error) {
33
+ const message = error instanceof Error ? error.message : String(error);
34
+ notify(ctx, `Advisor activation failed: ${message}`, "error");
35
+ }
36
+ });
37
+
38
+ runtime.pi.on("model_select", (event, ctx) => {
39
+ // "restore" replays a stored session model and "cycle" changes the active
40
+ // model without an explicit `/model` choice. Neither should redefine the
41
+ // configured Executor.
42
+ if (event.source !== "set" || runtime.suppressModelSelectionSync) {
43
+ return;
44
+ }
45
+ const selected = `${event.model.provider}/${event.model.id}`;
46
+ if (!runtime.flowEnabled()) {
47
+ // Defer persistence until `/advisor` succeeds. This keeps ordinary model
48
+ // selection global defaults untouched when the flow is not enabled.
49
+ runtime.pendingExecutorModelRef = selected;
50
+ return;
51
+ }
52
+ runtime.pendingExecutorModelRef = undefined;
53
+ if (selected === executorRef) {
54
+ return;
55
+ }
56
+ const persisted = getPersistedModelRefs();
57
+ setExecutorRef(selected);
58
+ saveConfig(ctx, {
59
+ persistAdvisor: Boolean(persisted.advisor),
60
+ persistExecutor: true,
61
+ });
62
+ });
63
+
64
+ runtime.pi.on("session_shutdown", (_event, ctx) => {
65
+ if (ctx.hasUI) {
66
+ ctx.ui.setStatus("advisor-usage", undefined);
67
+ }
68
+ for (const [controller, token] of runtime.manualConsultations) {
69
+ controller.abort();
70
+ const timer = runtime.manualProgressTimers.get(controller);
71
+ if (timer) {
72
+ clearInterval(timer);
73
+ runtime.manualProgressTimers.delete(controller);
74
+ }
75
+ runtime.scoutStatus.release(ctx, token);
76
+ }
77
+ runtime.scoutStatus.clear(ctx);
78
+ runtime.manualConsultations.clear();
79
+ runtime.manualProgressTimers.clear();
80
+ runtime.manualProgress.clear();
81
+ herdrAdvisorActivity.clear();
82
+ });
83
+ };
@@ -0,0 +1,107 @@
1
+ import {
2
+ getAdvisorMaxCallsPerSession,
3
+ getAdvisorSettings,
4
+ isSimpleMode,
5
+ } from "../config/state.js";
6
+ import type { GitContextLevel } from "../git.js";
7
+ import { resolveAdvisorRequest } from "../tools/render-common.js";
8
+ import { ManualAdvisorDialog } from "../ui/manual-dialog.js";
9
+ import type { ManualAdvisorRequest } from "../ui/types.js";
10
+ import { loadCommandConfig } from "./activation-preparation.js";
11
+ import { startManualConsultation } from "./manual-consultation.js";
12
+ import type { CommandRuntime, ManualAdvisorProgressState } from "./types.js";
13
+
14
+ export const registerManualCommand = (runtime: CommandRuntime) => {
15
+ runtime.pi.registerCommand("advisor-manual", {
16
+ description:
17
+ "Consult the Advisor in parallel; accepts an optional focused question and fans its response out to the Executor",
18
+ handler: async (args, ctx) => {
19
+ if (!loadCommandConfig(ctx)) {
20
+ return;
21
+ }
22
+ if (
23
+ !(
24
+ isSimpleMode() ||
25
+ runtime.advisorSessionState.canConsult(getAdvisorMaxCallsPerSession())
26
+ )
27
+ ) {
28
+ runtime.reportManualBudgetExhausted(ctx);
29
+ return;
30
+ }
31
+
32
+ let gitContext: GitContextLevel | undefined;
33
+ let question: string | undefined;
34
+ if (ctx.mode === "tui") {
35
+ const request = await ctx.ui.custom<ManualAdvisorRequest | undefined>(
36
+ (tui, theme, keybindings, done) =>
37
+ new ManualAdvisorDialog({
38
+ gitContext: getAdvisorSettings().gitContext,
39
+ initialMessage: args,
40
+ keybindings,
41
+ onCancel: () => done(undefined),
42
+ onSubmit: done,
43
+ theme,
44
+ tui,
45
+ }),
46
+ {
47
+ overlay: true,
48
+ overlayOptions: {
49
+ anchor: "center",
50
+ margin: 2,
51
+ maxHeight: "80%",
52
+ minWidth: 56,
53
+ width: 76,
54
+ },
55
+ }
56
+ );
57
+ if (!request) {
58
+ return;
59
+ }
60
+ if (
61
+ !(
62
+ isSimpleMode() ||
63
+ runtime.advisorSessionState.canConsult(
64
+ getAdvisorMaxCallsPerSession()
65
+ )
66
+ )
67
+ ) {
68
+ runtime.reportManualBudgetExhausted(ctx);
69
+ return;
70
+ }
71
+ const { gitContext: selectedGitContext, message } = request;
72
+ gitContext = selectedGitContext;
73
+ question = resolveAdvisorRequest(message);
74
+ } else {
75
+ question = resolveAdvisorRequest(args);
76
+ }
77
+
78
+ if (!isSimpleMode()) {
79
+ runtime.advisorSessionState.consumeCall();
80
+ }
81
+ // A single visible progress surface avoids competing consultations overwriting
82
+ // each other's streamed state. A newer manual request replaces the previous one.
83
+ for (const [pending, token] of runtime.manualConsultations) {
84
+ pending.abort();
85
+ runtime.scoutStatus.release(ctx, token);
86
+ }
87
+ runtime.manualConsultations.clear();
88
+ const controller = new AbortController();
89
+ const scoutStatusToken = Symbol("manual-scout");
90
+ const progressId = runtime.nextManualProgressId();
91
+ const progress: ManualAdvisorProgressState = { phase: "preparing" };
92
+ runtime.manualProgress.set(progressId, progress);
93
+ runtime.scoutStatus.register(scoutStatusToken);
94
+ runtime.manualConsultations.set(controller, scoutStatusToken);
95
+ runtime.pi.appendEntry?.("advisor-manual-call", { progressId, question });
96
+ startManualConsultation(
97
+ runtime,
98
+ ctx,
99
+ question,
100
+ controller,
101
+ scoutStatusToken,
102
+ progress,
103
+ gitContext
104
+ );
105
+ },
106
+ });
107
+ };
@@ -0,0 +1,144 @@
1
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ import { advisorRef } from "../config/state.js";
3
+ import type { GitContextLevel } from "../git.js";
4
+ import { herdrAdvisorActivity, notifyHerdrAdvisorFailure } from "../herdr.js";
5
+ import { appendScoutLifecycleEntry } from "../tools/scout-status.js";
6
+ import type { ScoutToolDetails } from "../tools/types.js";
7
+ import { advisorUsageCost, snapshotAdvisorUsage } from "../usage.js";
8
+ import { notify } from "./runtime.js";
9
+ import type { CommandRuntime, ManualAdvisorProgressState } from "./types.js";
10
+
11
+ export const startManualConsultation = (
12
+ runtime: CommandRuntime,
13
+ ctx: ExtensionContext,
14
+ question: string | undefined,
15
+ controller: AbortController,
16
+ scoutStatusToken: symbol,
17
+ progress: ManualAdvisorProgressState,
18
+ gitContext?: GitContextLevel
19
+ ) => {
20
+ herdrAdvisorActivity.start();
21
+ progress.phase = "preparing";
22
+ runtime.requestManualRender(ctx);
23
+ if (ctx.hasUI) {
24
+ const timer = setInterval(() => {
25
+ if (
26
+ controller.signal.aborted ||
27
+ runtime.manualConsultations.get(controller) !== scoutStatusToken
28
+ ) {
29
+ clearInterval(timer);
30
+ return;
31
+ }
32
+ runtime.requestManualRender(ctx);
33
+ }, 80);
34
+ runtime.manualProgressTimers.set(controller, timer);
35
+ }
36
+ let scoutDetails: ScoutToolDetails | undefined;
37
+ return runtime
38
+ .requestAdvisor(
39
+ ctx,
40
+ question,
41
+ controller.signal,
42
+ (thinking, text) => {
43
+ if (controller.signal.aborted) {
44
+ return;
45
+ }
46
+ progress.phase = "active";
47
+ progress.thinking = thinking;
48
+ progress.text = text;
49
+ runtime.requestManualRender(ctx);
50
+ },
51
+ (event) => {
52
+ if (!controller.signal.aborted) {
53
+ runtime.scoutStatus.update(ctx, scoutStatusToken, event);
54
+ scoutDetails = appendScoutLifecycleEntry(
55
+ runtime.pi,
56
+ event,
57
+ scoutDetails
58
+ );
59
+ progress.scout = scoutDetails;
60
+ progress.phase = "active";
61
+ runtime.requestManualRender(ctx);
62
+ }
63
+ },
64
+ gitContext
65
+ )
66
+ .then(({ markdown, usage }) => {
67
+ if (controller.signal.aborted) {
68
+ return;
69
+ }
70
+ progress.phase = "complete";
71
+ runtime.advisorSessionState.recordInvocation({
72
+ cost: advisorUsageCost(usage),
73
+ executionEffect: "continued",
74
+ kind: "markdown",
75
+ model: advisorRef,
76
+ trigger: "manual",
77
+ usage,
78
+ });
79
+ runtime.updateAdvisorUsageStatus(ctx);
80
+ const normalizedUsage = snapshotAdvisorUsage(usage);
81
+ runtime.pi.sendMessage(
82
+ {
83
+ content: `Manual Advisor consultation${question ? ` (${question})` : ""}:\n\n${markdown}`,
84
+ customType: "advisor-manual-result",
85
+ details: {
86
+ advisor: advisorRef,
87
+ question,
88
+ text: markdown,
89
+ ...(normalizedUsage ? { usage: normalizedUsage } : {}),
90
+ },
91
+ display: true,
92
+ },
93
+ {
94
+ // Steer lets the current turn finish its active work; the Executor sees
95
+ // the result before its next model call rather than being interrupted.
96
+ deliverAs: "steer",
97
+ triggerTurn: true,
98
+ }
99
+ );
100
+ })
101
+ .catch((error: unknown) => {
102
+ if (controller.signal.aborted) {
103
+ return;
104
+ }
105
+ progress.phase = "error";
106
+ const message = error instanceof Error ? error.message : String(error);
107
+ runtime.advisorSessionState.recordInvocation({
108
+ executionEffect: "continued",
109
+ failure: "provider-error",
110
+ kind: "markdown",
111
+ model: advisorRef,
112
+ trigger: "manual",
113
+ });
114
+ runtime.updateAdvisorUsageStatus(ctx);
115
+ runtime.pi.sendMessage(
116
+ {
117
+ content: `Manual Advisor consultation failed: ${message}`,
118
+ customType: "advisor-manual-result",
119
+ details: {
120
+ advisor: advisorRef,
121
+ text: `**Advisor consultation failed:** ${message}`,
122
+ },
123
+ display: true,
124
+ },
125
+ { deliverAs: "steer", triggerTurn: true }
126
+ );
127
+ notify(ctx, `Advisor consultation failed: ${message}`, "error");
128
+ notifyHerdrAdvisorFailure("Advisor consultation failed", message);
129
+ })
130
+ .finally(() => {
131
+ if (controller.signal.aborted) {
132
+ progress.phase = "cancelled";
133
+ }
134
+ const timer = runtime.manualProgressTimers.get(controller);
135
+ if (timer) {
136
+ clearInterval(timer);
137
+ runtime.manualProgressTimers.delete(controller);
138
+ }
139
+ runtime.requestManualRender(ctx);
140
+ runtime.scoutStatus.release(ctx, scoutStatusToken);
141
+ runtime.manualConsultations.delete(controller);
142
+ herdrAdvisorActivity.finish();
143
+ });
144
+ };