pi-advisor-flow 0.5.0 → 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 +54 -0
  2. package/README.md +37 -71
  3. package/extensions/index.ts +7 -7
  4. package/package.json +19 -6
  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 -946
  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 -791
  28. package/src/conversation.ts +4 -2
  29. package/src/herdr.ts +1 -1
  30. package/src/model-stream.ts +25 -2
  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 -1811
  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
@@ -0,0 +1,27 @@
1
+ import type {
2
+ ExtensionAPI,
3
+ ExtensionContext,
4
+ } from "@earendil-works/pi-coding-agent";
5
+ import { activateAdvisor } from "./activation.js";
6
+ import { registerCommandLifecycle } from "./lifecycle.js";
7
+ import { registerManualCommand } from "./manual-command.js";
8
+ import { registerModelCommands } from "./model-commands.js";
9
+ import { registerCommandRenderers } from "./renderers.js";
10
+ import { createCommandRuntime } from "./runtime.js";
11
+ import { registerSettingsCommands } from "./settings-commands.js";
12
+ import type { CommandDependencies } from "./types.js";
13
+
14
+ export const registerCommands = (
15
+ pi: ExtensionAPI,
16
+ dependencies: CommandDependencies = {}
17
+ ) => {
18
+ const runtime = createCommandRuntime(pi, dependencies);
19
+ const activate = (args: string, ctx: ExtensionContext, announce = true) =>
20
+ activateAdvisor(runtime, args, ctx, announce);
21
+
22
+ registerCommandRenderers(runtime);
23
+ registerCommandLifecycle(runtime, activate);
24
+ registerManualCommand(runtime);
25
+ registerModelCommands(runtime);
26
+ registerSettingsCommands(runtime);
27
+ };
@@ -0,0 +1,76 @@
1
+ import { getMarkdownTheme } from "@earendil-works/pi-coding-agent";
2
+ import { Box, Markdown, Text } from "@earendil-works/pi-tui";
3
+ import { getAdvisorSettings } from "../config/state.js";
4
+ import {
5
+ adviceForDisplay,
6
+ hasSoundVerdict,
7
+ renderAdvisorCallBox,
8
+ renderAdvisorResponseHeader,
9
+ } from "../tools/render-common.js";
10
+ import { formatAdvisorUsage } from "../usage.js";
11
+ import { ManualAdvisorProgressComponent } from "./manual-progress.js";
12
+ import type { CommandRuntime } from "./types.js";
13
+
14
+ export const registerCommandRenderers = (runtime: CommandRuntime) => {
15
+ runtime.pi.registerEntryRenderer?.(
16
+ "advisor-manual-call",
17
+ (entry, { expanded }, theme) => {
18
+ const { progressId, question } = (entry.data ?? {}) as {
19
+ progressId?: string;
20
+ question?: string;
21
+ };
22
+ const progress = progressId
23
+ ? runtime.manualProgress.get(progressId)
24
+ : undefined;
25
+ return progress
26
+ ? new ManualAdvisorProgressComponent(
27
+ question,
28
+ progress,
29
+ Boolean(expanded),
30
+ theme
31
+ )
32
+ : renderAdvisorCallBox(question, theme);
33
+ }
34
+ );
35
+
36
+ runtime.pi.registerMessageRenderer?.(
37
+ "advisor-manual-result",
38
+ (message, { expanded }, theme) => {
39
+ const details = message.details as
40
+ | { advisor?: string; text?: string; usage?: unknown }
41
+ | undefined;
42
+ const box = new Box(1, 1, (text) => theme.bg("customMessageBg", text));
43
+ const advice =
44
+ details?.text ??
45
+ (typeof message.content === "string"
46
+ ? message.content
47
+ : "(Advisor returned no advice.)");
48
+ // Manual consultations must render exactly like an Executor ask_advisor call.
49
+ box.addChild(
50
+ new Text(
51
+ renderAdvisorResponseHeader(hasSoundVerdict(advice), theme),
52
+ 0,
53
+ 0
54
+ )
55
+ );
56
+ if (details?.advisor) {
57
+ box.addChild(new Text(theme.fg("dim", ` ${details.advisor}`), 0, 0));
58
+ }
59
+ if (getAdvisorSettings().showUsageDetails) {
60
+ const usage = formatAdvisorUsage(details?.usage);
61
+ if (usage) {
62
+ box.addChild(new Text(theme.fg("dim", ` Usage: ${usage}`), 0, 0));
63
+ }
64
+ }
65
+ box.addChild(
66
+ new Markdown(
67
+ adviceForDisplay(advice, expanded),
68
+ 0,
69
+ 0,
70
+ getMarkdownTheme()
71
+ )
72
+ );
73
+ return box;
74
+ }
75
+ );
76
+ };
@@ -0,0 +1,114 @@
1
+ import type {
2
+ ExtensionAPI,
3
+ ExtensionContext,
4
+ } from "@earendil-works/pi-coding-agent";
5
+ import { getAdvisorSettings } from "../config/state.js";
6
+ import { notifyHerdrAdvisorFailure } from "../herdr.js";
7
+ import type { AdvisorSessionState } from "../session-state.js";
8
+ import { consultAdvisor } from "../tools/consultation.js";
9
+ import { ScoutStatusManager } from "../tools/scout-status.js";
10
+ import { advisorSessionState as defaultAdvisorSessionState } from "../tools/session.js";
11
+ import type {
12
+ CommandDependencies,
13
+ CommandRuntime as CommandRuntimeContract,
14
+ ManualAdvisorProgressState,
15
+ ManualConsult,
16
+ } from "./types.js";
17
+
18
+ export const notify = (
19
+ ctx: ExtensionContext,
20
+ message: string,
21
+ level: "error" | "info" | "warning"
22
+ ) => {
23
+ if (ctx.hasUI) {
24
+ ctx.ui.notify(message, level);
25
+ }
26
+ };
27
+
28
+ export class CommandRuntime implements CommandRuntimeContract {
29
+ readonly advisorSessionState: AdvisorSessionState;
30
+ readonly manualConsultations = new Map<AbortController, symbol>();
31
+ readonly manualProgress = new Map<string, ManualAdvisorProgressState>();
32
+ readonly manualProgressTimers = new Map<
33
+ AbortController,
34
+ ReturnType<typeof setInterval>
35
+ >();
36
+ readonly pi: ExtensionAPI;
37
+ readonly requestAdvisor: ManualConsult;
38
+ readonly scoutStatus: ScoutStatusManager;
39
+ manualProgressSequence = 0;
40
+ pendingExecutorModelRef: string | undefined;
41
+ suppressModelSelectionSync = false;
42
+
43
+ constructor(pi: ExtensionAPI, dependencies: CommandDependencies = {}) {
44
+ this.pi = pi;
45
+ this.advisorSessionState =
46
+ dependencies.sessionState ?? defaultAdvisorSessionState;
47
+ this.scoutStatus =
48
+ dependencies.statusManager ?? new ScoutStatusManager(false);
49
+ this.requestAdvisor =
50
+ dependencies.consult ??
51
+ ((ctx, question, signal, onChunk, onScout, gitContext) =>
52
+ consultAdvisor(
53
+ ctx,
54
+ question,
55
+ signal,
56
+ onChunk,
57
+ "manual",
58
+ gitContext,
59
+ undefined,
60
+ undefined,
61
+ undefined,
62
+ onScout,
63
+ undefined
64
+ ));
65
+ }
66
+
67
+ flowEnabled() {
68
+ return this.pi.getActiveTools().includes("ask_advisor");
69
+ }
70
+
71
+ nextManualProgressId() {
72
+ this.manualProgressSequence += 1;
73
+ return `manual-${this.manualProgressSequence}`;
74
+ }
75
+
76
+ async setExecutorModel(
77
+ model: Parameters<ExtensionAPI["setModel"]>[0]
78
+ ): Promise<boolean> {
79
+ this.suppressModelSelectionSync = true;
80
+ try {
81
+ return await this.pi.setModel(model);
82
+ } finally {
83
+ this.suppressModelSelectionSync = false;
84
+ }
85
+ }
86
+
87
+ updateAdvisorUsageStatus(ctx: ExtensionContext) {
88
+ if (ctx.hasUI) {
89
+ ctx.ui.setStatus(
90
+ "advisor-usage",
91
+ getAdvisorSettings().showUsageFooter
92
+ ? this.advisorSessionState.usageStatus()
93
+ : undefined
94
+ );
95
+ }
96
+ }
97
+
98
+ reportManualBudgetExhausted(ctx: ExtensionContext) {
99
+ const message = "Advisor call budget exhausted for this session.";
100
+ notify(ctx, message, "warning");
101
+ notifyHerdrAdvisorFailure("Advisor budget exhausted", message);
102
+ }
103
+
104
+ requestManualRender(ctx: ExtensionContext) {
105
+ if (ctx.hasUI) {
106
+ ctx.ui.setStatus("advisor-manual", undefined);
107
+ }
108
+ }
109
+ }
110
+
111
+ export const createCommandRuntime = (
112
+ pi: ExtensionAPI,
113
+ dependencies: CommandDependencies = {}
114
+ ) => new CommandRuntime(pi, dependencies);
@@ -0,0 +1,80 @@
1
+ import {
2
+ alwaysOnRef,
3
+ getAdvisorSettings,
4
+ getPersistedModelRefs,
5
+ setAlwaysOnRef,
6
+ } from "../config/state.js";
7
+ import { saveConfig } from "../config/storage.js";
8
+ import { AdvisorSettingsSelector } from "../ui/settings-selector.js";
9
+ import { loadCommandConfig } from "./activation-preparation.js";
10
+ import { CONTEXT_PRESETS, EFFORT_LEVELS } from "./model-options.js";
11
+ import { notify } from "./runtime.js";
12
+ import { saveAdvisorSettings } from "./settings-persistence.js";
13
+ import type { CommandRuntime } from "./types.js";
14
+
15
+ export const registerSettingsCommands = (runtime: CommandRuntime) => {
16
+ runtime.pi.registerCommand("advisor-settings", {
17
+ description: "Configure Advisor settings",
18
+ handler: async (_args, ctx) => {
19
+ if (!(loadCommandConfig(ctx) && ctx.hasUI)) {
20
+ return;
21
+ }
22
+
23
+ const initial = getAdvisorSettings();
24
+ await ctx.ui.custom<void>(
25
+ (tui, theme, _keybindings, done) =>
26
+ new AdvisorSettingsSelector({
27
+ effortLevels: EFFORT_LEVELS,
28
+ initial,
29
+ onCancel: () => done(),
30
+ onChange: (settings) => {
31
+ try {
32
+ saveAdvisorSettings(ctx, settings);
33
+ runtime.updateAdvisorUsageStatus(ctx);
34
+ } catch (error) {
35
+ const message =
36
+ error instanceof Error ? error.message : String(error);
37
+ ctx.ui.notify(
38
+ `Could not save Advisor settings: ${message}`,
39
+ "error"
40
+ );
41
+ }
42
+ },
43
+ presets: CONTEXT_PRESETS,
44
+ theme,
45
+ tui,
46
+ })
47
+ );
48
+ },
49
+ });
50
+
51
+ runtime.pi.registerCommand("advisor-off", {
52
+ description: "Disable on-demand Advisor calls; keep the current model",
53
+ handler: (_args, ctx) => {
54
+ runtime.pi.setActiveTools(
55
+ runtime.pi
56
+ .getActiveTools()
57
+ .filter(
58
+ (name) =>
59
+ name !== "ask_advisor" && name !== "record_advisor_outcome"
60
+ )
61
+ );
62
+ // Leaving alwaysOn set would silently reactivate the flow next session.
63
+ const wasAlwaysOn = alwaysOnRef;
64
+ if (wasAlwaysOn) {
65
+ const persisted = getPersistedModelRefs();
66
+ setAlwaysOnRef(false);
67
+ saveConfig(ctx, {
68
+ persistAdvisor: Boolean(persisted.advisor),
69
+ persistExecutor: Boolean(persisted.executor),
70
+ });
71
+ }
72
+ notify(
73
+ ctx,
74
+ `Advisor flow disabled. Current model unchanged.${wasAlwaysOn ? " Always on turned off." : ""}`,
75
+ "info"
76
+ );
77
+ return Promise.resolve();
78
+ },
79
+ });
80
+ };
@@ -0,0 +1,81 @@
1
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ import {
3
+ getPersistedModelRefs,
4
+ setAdvisorAutoLoopGateRef,
5
+ setAdvisorBlockOnBlockedRef,
6
+ setAdvisorCollapseResponsesRef,
7
+ setAdvisorCompletionGateRef,
8
+ setAdvisorCustomInvocationRef,
9
+ setAdvisorEffortRef,
10
+ setAdvisorFailureGateRef,
11
+ setAdvisorFailureModeRef,
12
+ setAdvisorGitContextMaxCharsRef,
13
+ setAdvisorGitContextRef,
14
+ setAdvisorHerdrIntegrationRef,
15
+ setAdvisorLoopThresholdRef,
16
+ setAdvisorMaxCallsPerSessionRef,
17
+ setAdvisorOutcomeLoggingRef,
18
+ setAdvisorPlanGateRef,
19
+ setAdvisorRedactSecretsRef,
20
+ setAdvisorScoutEnabledRef,
21
+ setAdvisorSessionSummaryRef,
22
+ setAdvisorToolPoliciesRef,
23
+ setAdvisorToolResultMaxBytesRef,
24
+ setAdvisorToolResultMaxLinesRef,
25
+ setAdvisorTrackedFileContentRef,
26
+ setAdvisorUntrackedContentRef,
27
+ setAlwaysOnRef,
28
+ setContextMaxCharsRef,
29
+ setShowUsageDetailsRef,
30
+ setShowUsageFooterRef,
31
+ setSimpleModeRef,
32
+ } from "../config/state.js";
33
+ import { saveConfig, saveGlobalOutcomeLogging } from "../config/storage.js";
34
+ import type { AdvisorSettings } from "../ui/types.js";
35
+
36
+ // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: one settings form maps every persisted control.
37
+ export const applyAdvisorSettings = (settings: AdvisorSettings) => {
38
+ setAdvisorEffortRef(
39
+ settings.effort === "Default (Model Default)" ? undefined : settings.effort
40
+ );
41
+ setContextMaxCharsRef(settings.contextMaxChars);
42
+ setAdvisorPlanGateRef(settings.planGate);
43
+ setAdvisorFailureGateRef(settings.failureGate);
44
+ setAdvisorCompletionGateRef(settings.completionGate);
45
+ setAdvisorCollapseResponsesRef(settings.collapseResponses);
46
+ setAdvisorCustomInvocationRef(settings.customRule);
47
+ setAdvisorBlockOnBlockedRef(settings.blockOnBlocked ?? true);
48
+ setAdvisorAutoLoopGateRef(settings.autoLoopGate ?? true);
49
+ setAdvisorLoopThresholdRef(settings.loopThreshold ?? 3);
50
+ setAdvisorMaxCallsPerSessionRef(settings.maxCallsPerSession);
51
+ setAdvisorSessionSummaryRef(settings.sessionSummary ?? false);
52
+ setAdvisorScoutEnabledRef(settings.scoutEnabled ?? false);
53
+ setShowUsageDetailsRef(settings.showUsageDetails ?? true);
54
+ setShowUsageFooterRef(settings.showUsageFooter ?? false);
55
+ setSimpleModeRef(settings.simpleMode ?? false);
56
+ setAlwaysOnRef(settings.alwaysOn ?? false);
57
+ setAdvisorFailureModeRef(settings.failureMode ?? "block-session");
58
+ setAdvisorHerdrIntegrationRef(settings.herdrIntegration ?? true);
59
+ setAdvisorToolResultMaxLinesRef(settings.toolResultMaxLines ?? 2000);
60
+ setAdvisorToolResultMaxBytesRef(settings.toolResultMaxBytes ?? 50 * 1024);
61
+ setAdvisorRedactSecretsRef(settings.redactSecrets ?? false);
62
+ setAdvisorGitContextRef(settings.gitContext ?? "summary");
63
+ setAdvisorGitContextMaxCharsRef(settings.gitContextMaxChars ?? 20_000);
64
+ setAdvisorToolPoliciesRef(settings.toolPolicies ?? {});
65
+ setAdvisorTrackedFileContentRef(settings.trackedFileContent ?? false);
66
+ setAdvisorUntrackedContentRef(settings.untrackedContent ?? false);
67
+ setAdvisorOutcomeLoggingRef(settings.outcomeLogging ?? false);
68
+ };
69
+
70
+ export const saveAdvisorSettings = (
71
+ ctx: ExtensionContext,
72
+ settings: AdvisorSettings
73
+ ) => {
74
+ applyAdvisorSettings(settings);
75
+ const persisted = getPersistedModelRefs();
76
+ saveConfig(ctx, {
77
+ persistAdvisor: Boolean(persisted.advisor),
78
+ persistExecutor: Boolean(persisted.executor),
79
+ });
80
+ saveGlobalOutcomeLogging(settings.outcomeLogging ?? false);
81
+ };
@@ -0,0 +1,78 @@
1
+ import type {
2
+ ExtensionAPI,
3
+ ExtensionContext,
4
+ Theme,
5
+ } from "@earendil-works/pi-coding-agent";
6
+ import type { GitContextLevel } from "../git.js";
7
+ import type { ScoutLifecycleEvent } from "../scout.js";
8
+ import type { AdvisorSessionState } from "../session-state.js";
9
+ import type { ScoutStatusManager } from "../tools/scout-status.js";
10
+ import type { ScoutToolDetails } from "../tools/types.js";
11
+
12
+ export type ManualConsult = (
13
+ ctx: ExtensionContext,
14
+ question?: string,
15
+ signal?: AbortSignal,
16
+ onChunk?: (thinking: string, text: string) => void,
17
+ onScout?: (event: ScoutLifecycleEvent) => void,
18
+ gitContext?: GitContextLevel
19
+ ) => Promise<{
20
+ markdown: string;
21
+ thinkingText: string;
22
+ draftBytes?: number;
23
+ preferenceBytes?: number;
24
+ usage?: unknown;
25
+ }>;
26
+
27
+ export type ThinkingLevel = Parameters<ExtensionAPI["setThinkingLevel"]>[0];
28
+
29
+ export type ManualAdvisorProgressPhase =
30
+ | "preparing"
31
+ | "active"
32
+ | "complete"
33
+ | "cancelled"
34
+ | "error";
35
+
36
+ export interface ManualAdvisorProgressState {
37
+ phase: ManualAdvisorProgressPhase;
38
+ scout?: ScoutToolDetails;
39
+ text?: string;
40
+ thinking?: string;
41
+ }
42
+
43
+ export interface CommandDependencies {
44
+ consult?: ManualConsult;
45
+ sessionState?: AdvisorSessionState;
46
+ statusManager?: ScoutStatusManager;
47
+ }
48
+
49
+ export interface CommandRuntime {
50
+ readonly advisorSessionState: AdvisorSessionState;
51
+ flowEnabled: () => boolean;
52
+ readonly manualConsultations: Map<AbortController, symbol>;
53
+ readonly manualProgress: Map<string, ManualAdvisorProgressState>;
54
+ manualProgressSequence: number;
55
+ readonly manualProgressTimers: Map<
56
+ AbortController,
57
+ ReturnType<typeof setInterval>
58
+ >;
59
+ nextManualProgressId: () => string;
60
+ pendingExecutorModelRef: string | undefined;
61
+ readonly pi: ExtensionAPI;
62
+ reportManualBudgetExhausted: (ctx: ExtensionContext) => void;
63
+ readonly requestAdvisor: ManualConsult;
64
+ requestManualRender: (ctx: ExtensionContext) => void;
65
+ readonly scoutStatus: ScoutStatusManager;
66
+ setExecutorModel: (
67
+ model: Parameters<ExtensionAPI["setModel"]>[0]
68
+ ) => ReturnType<ExtensionAPI["setModel"]>;
69
+ suppressModelSelectionSync: boolean;
70
+ updateAdvisorUsageStatus: (ctx: ExtensionContext) => void;
71
+ }
72
+
73
+ export interface ManualAdvisorProgressView {
74
+ expanded: boolean;
75
+ question: string | undefined;
76
+ state: ManualAdvisorProgressState;
77
+ theme: Theme;
78
+ }