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.
- package/CHANGELOG.md +14 -0
- package/README.md +3 -2
- package/extensions/index.ts +7 -7
- package/package.json +2 -1
- package/src/commands/activation-preparation.ts +117 -0
- package/src/commands/activation.ts +135 -0
- package/src/commands/lifecycle.ts +83 -0
- package/src/commands/manual-command.ts +107 -0
- package/src/commands/manual-consultation.ts +144 -0
- package/src/commands/manual-progress.ts +93 -0
- package/src/commands/model-commands.ts +66 -0
- package/src/commands/model-options.ts +181 -0
- package/src/commands/model-picker.ts +110 -0
- package/src/commands/registration.ts +27 -0
- package/src/commands/renderers.ts +76 -0
- package/src/commands/runtime.ts +114 -0
- package/src/commands/settings-commands.ts +80 -0
- package/src/commands/settings-persistence.ts +81 -0
- package/src/commands/types.ts +78 -0
- package/src/commands.ts +9 -1259
- package/src/config/args.ts +37 -0
- package/src/config/defaults.ts +190 -0
- package/src/config/state.ts +199 -0
- package/src/config/storage.ts +224 -0
- package/src/config/types.ts +63 -0
- package/src/config/validation.ts +234 -0
- package/src/config.ts +104 -814
- package/src/conversation.ts +4 -2
- package/src/herdr.ts +1 -1
- package/src/model-stream.ts +15 -1
- package/src/scout-context.ts +2 -2
- package/src/scout.ts +1 -1
- package/src/tools/consultation.ts +337 -0
- package/src/tools/gate-policy.ts +132 -0
- package/src/tools/gate-protocol.ts +127 -0
- package/src/tools/loop-gate.ts +210 -0
- package/src/tools/prompts.ts +159 -0
- package/src/tools/register-ask-advisor.ts +225 -0
- package/src/tools/register-lifecycle.ts +89 -0
- package/src/tools/register-outcome.ts +75 -0
- package/src/tools/register-renderers.ts +88 -0
- package/src/tools/registration.ts +35 -0
- package/src/tools/render-advisor-result.ts +168 -0
- package/src/tools/render-common.ts +120 -0
- package/src/tools/scout-status.ts +206 -0
- package/src/tools/session.ts +3 -0
- package/src/tools/types.ts +119 -0
- package/src/tools.ts +42 -1865
- package/src/ui/manual-dialog-render.ts +150 -0
- package/src/ui/manual-dialog.ts +305 -0
- package/src/ui/model-selector.ts +166 -0
- package/src/ui/settings-formatting.ts +133 -0
- package/src/ui/settings-items.ts +323 -0
- package/src/ui/settings-list-adapter.ts +103 -0
- package/src/ui/settings-mutations.ts +80 -0
- package/src/ui/settings-selector.ts +161 -0
- package/src/ui/text-setting-submenu.ts +66 -0
- package/src/ui/types.ts +111 -0
- 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
|
-
 
|
|
11
|
+
  
|
|
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,
|
|
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
|
|
package/extensions/index.ts
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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
|
+
};
|