pi-advisor-flow 0.1.6 → 0.1.8
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/README.md +18 -3
- package/package.json +4 -3
- package/src/commands.ts +31 -6
- package/src/config.ts +38 -1
- package/src/herdr.ts +126 -0
- package/src/session-state.ts +76 -0
- package/src/tools.ts +135 -12
- package/src/ui.ts +23 -3
package/README.md
CHANGED
|
@@ -53,7 +53,7 @@ Starts an Advisor consultation in parallel without interrupting the Executor's a
|
|
|
53
53
|
|
|
54
54
|
Opens a single keyboard-navigable settings screen. It includes a Claude Code-style context slider with `0`, `10k`, `25k`, `100k`, `200k`, and `ALL`; `0` sends no reconstructed history and `ALL` sends the complete current branch, subject to the Advisor model's context limit.
|
|
55
55
|
|
|
56
|
-
It also configures Advisor reasoning effort, whether long Advisor responses collapse to a short preview (`Ctrl+O` expands them), and each built-in invocation gate independently (consequential plans, repeated failures, and completion review). Response collapsing is off by default. You can add one custom natural-language invocation rule. Settings persist in `advisor.json`.
|
|
56
|
+
It also configures Advisor reasoning effort, whether long Advisor responses collapse to a short preview (`Ctrl+O` expands them), and each built-in invocation gate independently (consequential plans, repeated failures, and completion review). It includes controls for critical-response blocking, the automatic loop gate and its repeat threshold, a per-session Advisor-call limit, and the local Session Advisor Summary. Response collapsing is off by default. You can add one custom natural-language invocation rule. Settings persist in `advisor.json`.
|
|
57
57
|
|
|
58
58
|
### `/advisor-models`
|
|
59
59
|
|
|
@@ -68,6 +68,14 @@ Saves and persists your configuration to `~/.pi/agent/advisor.json`.
|
|
|
68
68
|
|
|
69
69
|
The Executor can call `ask_advisor` with an empty object for a general review of the current task and conversation, or provide `question` for targeted feedback. The Advisor is a brief second opinion: the Executor investigates and forms its own candidate direction first, then uses the Advisor to challenge assumptions and validate a consequential next step. It should not delegate the entire plan or task.
|
|
70
70
|
|
|
71
|
+
Advisor responses use a structured verdict: `proceed`, `revise`, `insufficient-evidence`, or `blocked`. A `blocked` verdict is reserved for critical escalation. When critical blocking is enabled (the default), it aborts the active run and marks the Pi pane blocked in Herdr.
|
|
72
|
+
|
|
73
|
+
### Automatic loop gate
|
|
74
|
+
|
|
75
|
+
When enabled, the loop gate consults the Advisor after the configured number of consecutive equivalent tool calls (default: three). A `proceed` verdict resets the repetition counter and allows the call. `revise` and `insufficient-evidence` stop only the repeated call and deliver the Advisor guidance to the Executor. A critical `blocked` verdict, an Advisor failure, or an exhausted Advisor-call budget blocks the session and reports that state to Herdr.
|
|
76
|
+
|
|
77
|
+
The maximum Advisor calls per session defaults to unlimited. When set to a finite value, the Executor receives the remaining-call count in its system instructions. The optional Session Advisor Summary is local, in-memory only, and appears after a non-blocked settled run; it is never persisted or sent to Herdr.
|
|
78
|
+
|
|
71
79
|
### Context configuration
|
|
72
80
|
|
|
73
81
|
The selected configuration is saved as `advisor.json` in the Pi agent directory (or an existing trusted project configuration). `/advisor-models` and `/advisor-settings` share this file:
|
|
@@ -83,11 +91,18 @@ The selected configuration is saved as `advisor.json` in the Pi agent directory
|
|
|
83
91
|
"advisorFailureGate": true,
|
|
84
92
|
"advisorCompletionGate": true,
|
|
85
93
|
"advisorCollapseResponses": false,
|
|
86
|
-
"advisorCustomInvocation": "before changing a production deployment"
|
|
94
|
+
"advisorCustomInvocation": "before changing a production deployment",
|
|
95
|
+
"advisorBlockOnBlocked": true,
|
|
96
|
+
"advisorAutoLoopGate": true,
|
|
97
|
+
"advisorLoopThreshold": 3,
|
|
98
|
+
"advisorMaxCallsPerSession": 5,
|
|
99
|
+
"advisorSessionSummary": true
|
|
87
100
|
}
|
|
88
101
|
```
|
|
89
102
|
|
|
90
|
-
All fields are optional. `executor`, `advisor`, and their effort settings are managed by `/advisor-models`. `/advisor-settings` manages `advisorEffort`, `contextMaxChars`, the three gate booleans, `advisorCollapseResponses`,
|
|
103
|
+
All fields are optional. `executor`, `advisor`, and their effort settings are managed by `/advisor-models`. `/advisor-settings` manages `advisorEffort`, `contextMaxChars`, the three invocation-gate booleans, `advisorCollapseResponses`, `advisorCustomInvocation`, `advisorBlockOnBlocked`, `advisorAutoLoopGate`, `advisorLoopThreshold`, `advisorMaxCallsPerSession`, and `advisorSessionSummary`.
|
|
104
|
+
|
|
105
|
+
`contextMaxChars` must be a non-negative safe integer: its default is 15,000, `0` omits history, and `9007199254740991` means ALL. `advisorLoopThreshold` must be at least `2` and defaults to `3`. Omit `advisorMaxCallsPerSession` for an unlimited session budget; otherwise it must be a non-negative safe integer. Critical blocking, the automatic loop gate, and the Session Advisor Summary default to `true`.
|
|
91
106
|
|
|
92
107
|
### `/advisor-off`
|
|
93
108
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-advisor-flow",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
},
|
|
9
9
|
"homepage": "https://github.com/philipbrembeck/pi-advisor",
|
|
10
10
|
"author": "Philip Brembeck",
|
|
11
|
-
"description": "Executor/Advisor flow for
|
|
11
|
+
"description": "Advanced Executor/Advisor flow for Pi, fully configurable and extendable.",
|
|
12
12
|
"type": "module",
|
|
13
13
|
"main": "extensions/index.ts",
|
|
14
14
|
"types": "extensions/index.ts",
|
|
@@ -17,7 +17,8 @@
|
|
|
17
17
|
"pi-extension",
|
|
18
18
|
"pi-coding-agent",
|
|
19
19
|
"advisor",
|
|
20
|
-
"pi-advisor"
|
|
20
|
+
"pi-advisor",
|
|
21
|
+
"herdr"
|
|
21
22
|
],
|
|
22
23
|
"files": [
|
|
23
24
|
"extensions",
|
package/src/commands.ts
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
|
-
import { getMarkdownTheme, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
1
|
+
import { getMarkdownTheme, type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { Box, Markdown, Text } from "@earendil-works/pi-tui";
|
|
3
3
|
import {
|
|
4
|
-
advisorCollapseResponsesRef, advisorCompletionGateRef, advisorCustomInvocationRef, advisorFailureGateRef, advisorPlanGateRef,
|
|
4
|
+
advisorAutoLoopGateRef, advisorMaxCallsPerSessionRef, advisorBlockOnBlockedRef, advisorCollapseResponsesRef, advisorCompletionGateRef, advisorCustomInvocationRef, advisorFailureGateRef, advisorLoopThresholdRef, advisorPlanGateRef, advisorSessionSummaryRef,
|
|
5
5
|
advisorRef, advisorEffortRef, executorRef, executorEffortRef,
|
|
6
|
-
setAdvisorCollapseResponsesRef, setAdvisorCompletionGateRef, setAdvisorCustomInvocationRef, setAdvisorFailureGateRef, setAdvisorPlanGateRef,
|
|
6
|
+
setAdvisorAutoLoopGateRef, setAdvisorMaxCallsPerSessionRef, setAdvisorBlockOnBlockedRef, setAdvisorCollapseResponsesRef, setAdvisorCompletionGateRef, setAdvisorCustomInvocationRef, setAdvisorFailureGateRef, setAdvisorLoopThresholdRef, setAdvisorPlanGateRef, setAdvisorSessionSummaryRef,
|
|
7
7
|
setAdvisorRef, setAdvisorEffortRef, setContextMaxCharsRef, setExecutorRef, setExecutorEffortRef,
|
|
8
8
|
contextMaxCharsRef, loadConfig, saveConfig, parseArgs, splitRef,
|
|
9
9
|
} from "./config.js";
|
|
10
10
|
import { AdvisorSettingsSelector, SearchableModelSelector, type AdvisorSettings, type ContextPreset } from "./ui.js";
|
|
11
|
-
import {
|
|
11
|
+
import { herdrAdvisorActivity } from "./herdr.js";
|
|
12
|
+
import { adviceForDisplay, advisorSessionState, consult, renderAdvisorCallBox, resolveAdvisorRequest } from "./tools.js";
|
|
12
13
|
|
|
13
14
|
const EFFORT_LEVELS = ["Default (Model Default)", "off", "minimal", "low", "medium", "high", "xhigh", "max"];
|
|
14
15
|
|
|
@@ -21,7 +22,9 @@ const CONTEXT_PRESETS: ContextPreset[] = [
|
|
|
21
22
|
{ label: "ALL", value: Number.MAX_SAFE_INTEGER, description: "The complete reconstructed conversation branch. Cost and model context limits apply." },
|
|
22
23
|
];
|
|
23
24
|
|
|
24
|
-
|
|
25
|
+
type ManualConsult = (ctx: ExtensionContext, question?: string, signal?: AbortSignal) => Promise<{ advice: string; thinkingText: string }>;
|
|
26
|
+
|
|
27
|
+
export const registerCommands = (pi: ExtensionAPI, dependencies: { consult?: ManualConsult } = {}) => {
|
|
25
28
|
const flowEnabled = () => pi.getActiveTools().includes("ask_advisor");
|
|
26
29
|
const requestAdvisor = dependencies.consult ?? consult;
|
|
27
30
|
const manualConsultations = new Set<AbortController>();
|
|
@@ -44,11 +47,18 @@ export const registerCommands = (pi: ExtensionAPI, dependencies: { consult?: typ
|
|
|
44
47
|
pi.on("session_shutdown", () => {
|
|
45
48
|
for (const controller of manualConsultations) controller.abort();
|
|
46
49
|
manualConsultations.clear();
|
|
50
|
+
herdrAdvisorActivity.clear();
|
|
47
51
|
});
|
|
48
52
|
|
|
49
53
|
pi.registerCommand("advisor-manual", {
|
|
50
54
|
description: "Consult the Advisor in parallel; accepts an optional focused question and fans its response out to the Executor",
|
|
51
55
|
handler: async (args, ctx) => {
|
|
56
|
+
loadConfig(ctx);
|
|
57
|
+
if (!advisorSessionState.canUseAutomaticCall(advisorMaxCallsPerSessionRef)) {
|
|
58
|
+
if (ctx.hasUI) ctx.ui.notify("Advisor call budget exhausted for this session.", "warning");
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
advisorSessionState.consumeAutomaticCall();
|
|
52
62
|
const question = resolveAdvisorRequest(args);
|
|
53
63
|
// A single visible progress surface avoids competing consultations overwriting
|
|
54
64
|
// each other's streamed state. A newer manual request replaces the previous one.
|
|
@@ -58,8 +68,10 @@ export const registerCommands = (pi: ExtensionAPI, dependencies: { consult?: typ
|
|
|
58
68
|
manualConsultations.add(controller);
|
|
59
69
|
pi.appendEntry?.("advisor-manual-call", { question });
|
|
60
70
|
|
|
71
|
+
herdrAdvisorActivity.start();
|
|
61
72
|
void requestAdvisor(ctx, question, controller.signal)
|
|
62
73
|
.then(({ advice }) => {
|
|
74
|
+
advisorSessionState.recordConsultation("manual");
|
|
63
75
|
if (controller.signal.aborted) return;
|
|
64
76
|
pi.sendMessage({
|
|
65
77
|
customType: "advisor-manual-result",
|
|
@@ -78,7 +90,10 @@ export const registerCommands = (pi: ExtensionAPI, dependencies: { consult?: typ
|
|
|
78
90
|
const message = error instanceof Error ? error.message : String(error);
|
|
79
91
|
if (ctx.hasUI) ctx.ui.notify(`Advisor consultation failed: ${message}`, "error");
|
|
80
92
|
})
|
|
81
|
-
.finally(() =>
|
|
93
|
+
.finally(() => {
|
|
94
|
+
manualConsultations.delete(controller);
|
|
95
|
+
herdrAdvisorActivity.finish();
|
|
96
|
+
});
|
|
82
97
|
},
|
|
83
98
|
});
|
|
84
99
|
|
|
@@ -149,6 +164,11 @@ export const registerCommands = (pi: ExtensionAPI, dependencies: { consult?: typ
|
|
|
149
164
|
completionGate: advisorCompletionGateRef,
|
|
150
165
|
collapseResponses: advisorCollapseResponsesRef,
|
|
151
166
|
customRule: advisorCustomInvocationRef,
|
|
167
|
+
blockOnBlocked: advisorBlockOnBlockedRef,
|
|
168
|
+
autoLoopGate: advisorAutoLoopGateRef,
|
|
169
|
+
loopThreshold: advisorLoopThresholdRef,
|
|
170
|
+
maxCallsPerSession: advisorMaxCallsPerSessionRef,
|
|
171
|
+
sessionSummary: advisorSessionSummaryRef,
|
|
152
172
|
};
|
|
153
173
|
const settings = await ctx.ui.custom<AdvisorSettings | undefined>((tui, theme, _keybindings, done) =>
|
|
154
174
|
new AdvisorSettingsSelector({
|
|
@@ -170,6 +190,11 @@ export const registerCommands = (pi: ExtensionAPI, dependencies: { consult?: typ
|
|
|
170
190
|
setAdvisorCompletionGateRef(settings.completionGate);
|
|
171
191
|
setAdvisorCollapseResponsesRef(settings.collapseResponses);
|
|
172
192
|
setAdvisorCustomInvocationRef(settings.customRule);
|
|
193
|
+
setAdvisorBlockOnBlockedRef(settings.blockOnBlocked ?? true);
|
|
194
|
+
setAdvisorAutoLoopGateRef(settings.autoLoopGate ?? true);
|
|
195
|
+
setAdvisorLoopThresholdRef(settings.loopThreshold ?? 3);
|
|
196
|
+
setAdvisorMaxCallsPerSessionRef(settings.maxCallsPerSession);
|
|
197
|
+
setAdvisorSessionSummaryRef(settings.sessionSummary ?? true);
|
|
173
198
|
const path = saveConfig(ctx);
|
|
174
199
|
ctx.ui.notify(`Saved Advisor settings to ${path}`, "info");
|
|
175
200
|
},
|
package/src/config.ts
CHANGED
|
@@ -18,6 +18,11 @@ export let advisorFailureGateRef = true;
|
|
|
18
18
|
export let advisorCompletionGateRef = true;
|
|
19
19
|
export let advisorCustomInvocationRef: string | undefined = undefined;
|
|
20
20
|
export let advisorCollapseResponsesRef = false;
|
|
21
|
+
export let advisorBlockOnBlockedRef = true;
|
|
22
|
+
export let advisorAutoLoopGateRef = true;
|
|
23
|
+
export let advisorLoopThresholdRef = 3;
|
|
24
|
+
export let advisorMaxCallsPerSessionRef: number | undefined = undefined;
|
|
25
|
+
export let advisorSessionSummaryRef = true;
|
|
21
26
|
|
|
22
27
|
export const setExecutorRef = (ref: string) => { executorRef = ref; };
|
|
23
28
|
export const setAdvisorRef = (ref: string) => { advisorRef = ref; };
|
|
@@ -31,6 +36,13 @@ export const setAdvisorFailureGateRef = (enabled: boolean) => { advisorFailureGa
|
|
|
31
36
|
export const setAdvisorCompletionGateRef = (enabled: boolean) => { advisorCompletionGateRef = enabled; };
|
|
32
37
|
export const setAdvisorCustomInvocationRef = (rule: string | undefined) => { advisorCustomInvocationRef = rule?.trim() || undefined; };
|
|
33
38
|
export const setAdvisorCollapseResponsesRef = (enabled: boolean) => { advisorCollapseResponsesRef = enabled; };
|
|
39
|
+
export const setAdvisorBlockOnBlockedRef = (enabled: boolean) => { advisorBlockOnBlockedRef = enabled; };
|
|
40
|
+
export const setAdvisorAutoLoopGateRef = (enabled: boolean) => { advisorAutoLoopGateRef = enabled; };
|
|
41
|
+
export const isValidLoopThreshold = (value: unknown): value is number => typeof value === "number" && Number.isSafeInteger(value) && value >= 2;
|
|
42
|
+
export const setAdvisorLoopThresholdRef = (value: number) => { advisorLoopThresholdRef = value; };
|
|
43
|
+
export const isValidMaxCallsPerSession = (value: unknown): value is number => typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
|
44
|
+
export const setAdvisorMaxCallsPerSessionRef = (value: number | undefined) => { advisorMaxCallsPerSessionRef = value; };
|
|
45
|
+
export const setAdvisorSessionSummaryRef = (enabled: boolean) => { advisorSessionSummaryRef = enabled; };
|
|
34
46
|
|
|
35
47
|
export const splitRef = (ref: string): [string, string] => {
|
|
36
48
|
const i = ref.indexOf("/");
|
|
@@ -53,6 +65,11 @@ type AdvisorConfig = {
|
|
|
53
65
|
advisorCompletionGate?: boolean;
|
|
54
66
|
advisorCustomInvocation?: string;
|
|
55
67
|
advisorCollapseResponses?: boolean;
|
|
68
|
+
advisorBlockOnBlocked?: boolean;
|
|
69
|
+
advisorAutoLoopGate?: boolean;
|
|
70
|
+
advisorLoopThreshold?: number;
|
|
71
|
+
advisorMaxCallsPerSession?: number;
|
|
72
|
+
advisorSessionSummary?: boolean;
|
|
56
73
|
};
|
|
57
74
|
|
|
58
75
|
const isValidConfig = (value: unknown): value is AdvisorConfig => {
|
|
@@ -67,7 +84,12 @@ const isValidConfig = (value: unknown): value is AdvisorConfig => {
|
|
|
67
84
|
&& (config.advisorFailureGate === undefined || typeof config.advisorFailureGate === "boolean")
|
|
68
85
|
&& (config.advisorCompletionGate === undefined || typeof config.advisorCompletionGate === "boolean")
|
|
69
86
|
&& (config.advisorCustomInvocation === undefined || typeof config.advisorCustomInvocation === "string")
|
|
70
|
-
&& (config.advisorCollapseResponses === undefined || typeof config.advisorCollapseResponses === "boolean")
|
|
87
|
+
&& (config.advisorCollapseResponses === undefined || typeof config.advisorCollapseResponses === "boolean")
|
|
88
|
+
&& (config.advisorBlockOnBlocked === undefined || typeof config.advisorBlockOnBlocked === "boolean")
|
|
89
|
+
&& (config.advisorAutoLoopGate === undefined || typeof config.advisorAutoLoopGate === "boolean")
|
|
90
|
+
&& (config.advisorLoopThreshold === undefined || isValidLoopThreshold(config.advisorLoopThreshold))
|
|
91
|
+
&& (config.advisorMaxCallsPerSession === undefined || isValidMaxCallsPerSession(config.advisorMaxCallsPerSession))
|
|
92
|
+
&& (config.advisorSessionSummary === undefined || typeof config.advisorSessionSummary === "boolean");
|
|
71
93
|
};
|
|
72
94
|
|
|
73
95
|
export const loadConfig = (ctx: ExtensionContext) => {
|
|
@@ -81,6 +103,11 @@ export const loadConfig = (ctx: ExtensionContext) => {
|
|
|
81
103
|
advisorCompletionGateRef = true;
|
|
82
104
|
advisorCustomInvocationRef = undefined;
|
|
83
105
|
advisorCollapseResponsesRef = false;
|
|
106
|
+
advisorBlockOnBlockedRef = true;
|
|
107
|
+
advisorAutoLoopGateRef = true;
|
|
108
|
+
advisorLoopThresholdRef = 3;
|
|
109
|
+
advisorMaxCallsPerSessionRef = undefined;
|
|
110
|
+
advisorSessionSummaryRef = true;
|
|
84
111
|
for (const path of configPaths(ctx)) {
|
|
85
112
|
if (!path || !existsSync(path)) continue;
|
|
86
113
|
try {
|
|
@@ -96,6 +123,11 @@ export const loadConfig = (ctx: ExtensionContext) => {
|
|
|
96
123
|
if (typeof config.advisorCompletionGate === "boolean") advisorCompletionGateRef = config.advisorCompletionGate;
|
|
97
124
|
if (typeof config.advisorCustomInvocation === "string") advisorCustomInvocationRef = config.advisorCustomInvocation || undefined;
|
|
98
125
|
if (typeof config.advisorCollapseResponses === "boolean") advisorCollapseResponsesRef = config.advisorCollapseResponses;
|
|
126
|
+
if (typeof config.advisorBlockOnBlocked === "boolean") advisorBlockOnBlockedRef = config.advisorBlockOnBlocked;
|
|
127
|
+
if (typeof config.advisorAutoLoopGate === "boolean") advisorAutoLoopGateRef = config.advisorAutoLoopGate;
|
|
128
|
+
if (isValidLoopThreshold(config.advisorLoopThreshold)) advisorLoopThresholdRef = config.advisorLoopThreshold;
|
|
129
|
+
if (isValidMaxCallsPerSession(config.advisorMaxCallsPerSession)) advisorMaxCallsPerSessionRef = config.advisorMaxCallsPerSession;
|
|
130
|
+
if (typeof config.advisorSessionSummary === "boolean") advisorSessionSummaryRef = config.advisorSessionSummary;
|
|
99
131
|
return path;
|
|
100
132
|
} catch {
|
|
101
133
|
// Ignore malformed config and keep looking for a valid fallback.
|
|
@@ -127,6 +159,11 @@ export const saveConfig = (ctx: ExtensionContext) => {
|
|
|
127
159
|
advisorCompletionGate: advisorCompletionGateRef,
|
|
128
160
|
advisorCustomInvocation: advisorCustomInvocationRef,
|
|
129
161
|
advisorCollapseResponses: advisorCollapseResponsesRef,
|
|
162
|
+
advisorBlockOnBlocked: advisorBlockOnBlockedRef,
|
|
163
|
+
advisorAutoLoopGate: advisorAutoLoopGateRef,
|
|
164
|
+
advisorLoopThreshold: advisorLoopThresholdRef,
|
|
165
|
+
...(advisorMaxCallsPerSessionRef === undefined ? {} : { advisorMaxCallsPerSession: advisorMaxCallsPerSessionRef }),
|
|
166
|
+
advisorSessionSummary: advisorSessionSummaryRef,
|
|
130
167
|
};
|
|
131
168
|
writeFileSync(path, `${JSON.stringify(data, null, 2)}\n`);
|
|
132
169
|
return path;
|
package/src/herdr.ts
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import net from "node:net";
|
|
2
|
+
|
|
3
|
+
const SOURCE = "pi-advisor:advisor-activity";
|
|
4
|
+
const BLOCK_SOURCE = "pi-advisor:advisor-block";
|
|
5
|
+
const HERDR_PI_SOURCE = "herdr:pi";
|
|
6
|
+
let sequence = Date.now() * 1_000;
|
|
7
|
+
|
|
8
|
+
const nextSequence = () => ++sequence;
|
|
9
|
+
|
|
10
|
+
type HerdrRequest = {
|
|
11
|
+
id: string;
|
|
12
|
+
method: "pane.report_metadata";
|
|
13
|
+
params: {
|
|
14
|
+
pane_id: string;
|
|
15
|
+
source: string;
|
|
16
|
+
agent: "pi";
|
|
17
|
+
applies_to_source: string;
|
|
18
|
+
state_labels?: { working?: string; blocked?: string };
|
|
19
|
+
clear_state_labels?: true;
|
|
20
|
+
seq: number;
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
type Report = (request: HerdrRequest) => void;
|
|
25
|
+
|
|
26
|
+
const sendToHerdr: Report = (request) => {
|
|
27
|
+
if (process.env.HERDR_ENV !== "1") return;
|
|
28
|
+
const paneId = process.env.HERDR_PANE_ID;
|
|
29
|
+
const socketPath = process.env.HERDR_SOCKET_PATH;
|
|
30
|
+
if (!paneId || !socketPath) return;
|
|
31
|
+
|
|
32
|
+
const endpoint = process.platform === "win32" ? `\\\\.\\pipe\\${socketPath}` : socketPath;
|
|
33
|
+
const socket = net.createConnection(endpoint);
|
|
34
|
+
const timeout = setTimeout(() => socket.destroy(), 500);
|
|
35
|
+
timeout.unref?.();
|
|
36
|
+
socket.once("connect", () => socket.write(`${JSON.stringify(request)}\n`));
|
|
37
|
+
socket.once("data", () => socket.destroy());
|
|
38
|
+
socket.once("error", () => socket.destroy());
|
|
39
|
+
socket.once("close", () => clearTimeout(timeout));
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export class HerdrAdvisorActivity {
|
|
43
|
+
#activeConsultations = 0;
|
|
44
|
+
|
|
45
|
+
constructor(private readonly report: Report = sendToHerdr) {}
|
|
46
|
+
|
|
47
|
+
start() {
|
|
48
|
+
this.#activeConsultations += 1;
|
|
49
|
+
if (this.#activeConsultations === 1) this.safeReport(false);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
finish() {
|
|
53
|
+
if (this.#activeConsultations === 0) return;
|
|
54
|
+
this.#activeConsultations -= 1;
|
|
55
|
+
if (this.#activeConsultations === 0) this.safeReport(true);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
clear() {
|
|
59
|
+
if (this.#activeConsultations === 0) return;
|
|
60
|
+
this.#activeConsultations = 0;
|
|
61
|
+
this.safeReport(true);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
private safeReport(clear: boolean) {
|
|
65
|
+
try {
|
|
66
|
+
this.report(this.request(clear));
|
|
67
|
+
} catch {
|
|
68
|
+
// Herdr is optional; an unavailable integration must never affect advice.
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
private request(clear: boolean): HerdrRequest {
|
|
73
|
+
return {
|
|
74
|
+
id: `${SOURCE}:${nextSequence()}`,
|
|
75
|
+
method: "pane.report_metadata",
|
|
76
|
+
params: {
|
|
77
|
+
pane_id: process.env.HERDR_PANE_ID ?? "",
|
|
78
|
+
source: SOURCE,
|
|
79
|
+
agent: "pi",
|
|
80
|
+
applies_to_source: HERDR_PI_SOURCE,
|
|
81
|
+
...(clear ? { clear_state_labels: true } : { state_labels: { working: "seeking advice" } }),
|
|
82
|
+
seq: nextSequence(),
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export class HerdrAdvisorBlock {
|
|
89
|
+
#blocked = false;
|
|
90
|
+
|
|
91
|
+
constructor(private readonly report: Report = sendToHerdr) {}
|
|
92
|
+
|
|
93
|
+
set(reason: string) {
|
|
94
|
+
this.#blocked = true;
|
|
95
|
+
this.safeReport({ blocked: reason });
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
clear() {
|
|
99
|
+
if (!this.#blocked) return;
|
|
100
|
+
this.#blocked = false;
|
|
101
|
+
try {
|
|
102
|
+
this.report({
|
|
103
|
+
id: `${BLOCK_SOURCE}:${nextSequence()}`,
|
|
104
|
+
method: "pane.report_metadata",
|
|
105
|
+
params: { pane_id: process.env.HERDR_PANE_ID ?? "", source: BLOCK_SOURCE, agent: "pi", applies_to_source: HERDR_PI_SOURCE, clear_state_labels: true, seq: nextSequence() },
|
|
106
|
+
});
|
|
107
|
+
} catch {
|
|
108
|
+
// Herdr remains optional.
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
private safeReport(labels: { blocked: string }) {
|
|
113
|
+
try {
|
|
114
|
+
this.report({
|
|
115
|
+
id: `${BLOCK_SOURCE}:${nextSequence()}`,
|
|
116
|
+
method: "pane.report_metadata",
|
|
117
|
+
params: { pane_id: process.env.HERDR_PANE_ID ?? "", source: BLOCK_SOURCE, agent: "pi", applies_to_source: HERDR_PI_SOURCE, state_labels: labels, seq: nextSequence() },
|
|
118
|
+
});
|
|
119
|
+
} catch {
|
|
120
|
+
// Herdr remains optional.
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export const herdrAdvisorActivity = new HerdrAdvisorActivity();
|
|
126
|
+
export const herdrAdvisorBlock = new HerdrAdvisorBlock();
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
export type AdvisorVerdict = "proceed" | "revise" | "blocked" | "insufficient-evidence";
|
|
2
|
+
export type ConsultationOrigin = "executor" | "automatic" | "manual";
|
|
3
|
+
|
|
4
|
+
type Consultation = { origin: ConsultationOrigin; verdict?: AdvisorVerdict };
|
|
5
|
+
|
|
6
|
+
const stableJson = (value: unknown): string => {
|
|
7
|
+
if (value === null || typeof value !== "object") return JSON.stringify(value);
|
|
8
|
+
if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
|
|
9
|
+
const record = value as Record<string, unknown>;
|
|
10
|
+
return `{${Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${stableJson(record[key])}`).join(",")}}`;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export class AdvisorSessionState {
|
|
14
|
+
#previousSignature?: string;
|
|
15
|
+
#repetitions = 0;
|
|
16
|
+
#blockedReason?: string;
|
|
17
|
+
#consultations: Consultation[] = [];
|
|
18
|
+
#loopInterventions = 0;
|
|
19
|
+
#automaticCalls = 0;
|
|
20
|
+
|
|
21
|
+
resetTask() {
|
|
22
|
+
this.#previousSignature = undefined;
|
|
23
|
+
this.#repetitions = 0;
|
|
24
|
+
this.#blockedReason = undefined;
|
|
25
|
+
this.#consultations = [];
|
|
26
|
+
this.#loopInterventions = 0;
|
|
27
|
+
this.#automaticCalls = 0;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
clearBlocked() { this.#blockedReason = undefined; }
|
|
31
|
+
resetRepetition() { this.#previousSignature = undefined; this.#repetitions = 0; }
|
|
32
|
+
get blocked() { return this.#blockedReason !== undefined; }
|
|
33
|
+
get blockedReason() { return this.#blockedReason; }
|
|
34
|
+
block(reason: string) { this.#blockedReason ??= reason; }
|
|
35
|
+
|
|
36
|
+
recordToolCall(toolName: string, input: unknown, threshold: number) {
|
|
37
|
+
if (toolName === "ask_advisor") return false;
|
|
38
|
+
const signature = `${toolName}:${stableJson(input)}`;
|
|
39
|
+
this.#repetitions = signature === this.#previousSignature ? this.#repetitions + 1 : 1;
|
|
40
|
+
this.#previousSignature = signature;
|
|
41
|
+
if (this.#repetitions < threshold) return false;
|
|
42
|
+
this.#loopInterventions += 1;
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
canUseAutomaticCall(limit: number | undefined) {
|
|
47
|
+
return limit === undefined || this.#automaticCalls < limit;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
consumeAutomaticCall() { this.#automaticCalls += 1; }
|
|
51
|
+
remainingAutomaticCalls(limit: number | undefined) {
|
|
52
|
+
return limit === undefined ? undefined : Math.max(0, limit - this.#automaticCalls);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
recordConsultation(origin: ConsultationOrigin, verdict?: AdvisorVerdict) {
|
|
56
|
+
this.#consultations.push({ origin, verdict });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
summary(limit: number | undefined) {
|
|
60
|
+
if (this.#consultations.length === 0 && this.#loopInterventions === 0) return undefined;
|
|
61
|
+
const count = (origin: ConsultationOrigin) => this.#consultations.filter((item) => item.origin === origin).length;
|
|
62
|
+
const verdicts = (["proceed", "revise", "insufficient-evidence", "blocked"] as AdvisorVerdict[])
|
|
63
|
+
.map((verdict) => [verdict, this.#consultations.filter((item) => item.verdict === verdict).length] as const)
|
|
64
|
+
.filter(([, count]) => count > 0)
|
|
65
|
+
.map(([verdict, count]) => `${count} ${verdict}`)
|
|
66
|
+
.join(", ") || "none recorded";
|
|
67
|
+
const budget = limit === undefined ? "unlimited" : `${this.#automaticCalls} / ${limit} used`;
|
|
68
|
+
return [
|
|
69
|
+
"[Session Advisor Summary]",
|
|
70
|
+
`Consultations: ${this.#consultations.length} — ${count("executor")} Executor-requested, ${count("automatic")} automatic, ${count("manual")} manual`,
|
|
71
|
+
`Verdicts: ${verdicts}`,
|
|
72
|
+
`Loop interventions: ${this.#loopInterventions}`,
|
|
73
|
+
`Automatic budget: ${budget}`,
|
|
74
|
+
].join("\n");
|
|
75
|
+
}
|
|
76
|
+
}
|
package/src/tools.ts
CHANGED
|
@@ -3,10 +3,14 @@ import { getMarkdownTheme, type ExtensionAPI, type ExtensionContext } from "@ear
|
|
|
3
3
|
import { Box, Markdown, Text } from "@earendil-works/pi-tui";
|
|
4
4
|
import { Type } from "typebox";
|
|
5
5
|
import {
|
|
6
|
-
advisorCollapseResponsesRef, advisorCompletionGateRef, advisorCustomInvocationRef, advisorFailureGateRef, advisorPlanGateRef,
|
|
6
|
+
advisorAutoLoopGateRef, advisorMaxCallsPerSessionRef, advisorBlockOnBlockedRef, advisorCollapseResponsesRef, advisorCompletionGateRef, advisorCustomInvocationRef, advisorFailureGateRef, advisorLoopThresholdRef, advisorPlanGateRef, advisorSessionSummaryRef,
|
|
7
7
|
advisorRef, advisorEffortRef, contextMaxCharsRef, loadConfig, splitRef,
|
|
8
8
|
} from "./config.js";
|
|
9
9
|
import { recentConversation, textFrom } from "./conversation.js";
|
|
10
|
+
import { herdrAdvisorActivity, herdrAdvisorBlock } from "./herdr.js";
|
|
11
|
+
import { AdvisorSessionState, type AdvisorVerdict } from "./session-state.js";
|
|
12
|
+
|
|
13
|
+
export const advisorSessionState = new AdvisorSessionState();
|
|
10
14
|
|
|
11
15
|
export const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
12
16
|
export const resolveAdvisorRequest = (question?: string) => question?.trim() || undefined;
|
|
@@ -45,9 +49,30 @@ export const ADVISOR_SYSTEM = [
|
|
|
45
49
|
"You already have the relevant reconstructed conversation context. No question or other input from the Executor is needed for a general review.",
|
|
46
50
|
"When no targeted focus is supplied, proactively review the task, risks, proposed direction, and validation from the context. Do not ask the Executor for a question, clarification, more input, or confirmation.",
|
|
47
51
|
"The context may be truncated, so state any material uncertainty and make the best recommendation you can from what is present.",
|
|
48
|
-
"You do not act or take over planning.
|
|
52
|
+
"You do not act or take over planning. Return only a JSON object with verdict (proceed, revise, insufficient-evidence, or blocked), criticalFindings (array of {severity, claim, evidence}), missingEvidence (string array), smallestNextStep (string), verificationRequired (string array), and escalationReason (string or null). Use blocked only for a critical issue requiring the user; never claim verification that the supplied evidence does not show.",
|
|
49
53
|
].join(" ");
|
|
50
54
|
|
|
55
|
+
type Advice = { verdict: AdvisorVerdict; criticalFindings: Array<{ severity: string; claim: string; evidence: string }>; missingEvidence: string[]; smallestNextStep: string; verificationRequired: string[]; escalationReason: string | null };
|
|
56
|
+
|
|
57
|
+
export const parseAdvice = (text: string): Advice => {
|
|
58
|
+
try {
|
|
59
|
+
const value = JSON.parse(text) as Partial<Advice>;
|
|
60
|
+
if (!["proceed", "revise", "blocked", "insufficient-evidence"].includes(value.verdict ?? "")) throw new Error("invalid verdict");
|
|
61
|
+
if (!Array.isArray(value.criticalFindings) || !Array.isArray(value.missingEvidence) || !Array.isArray(value.verificationRequired) || typeof value.smallestNextStep !== "string" || !value.missingEvidence.every((item) => typeof item === "string") || !value.verificationRequired.every((item) => typeof item === "string") || !value.criticalFindings.every((item) => item && typeof item === "object" && typeof (item as Record<string, unknown>).severity === "string" && typeof (item as Record<string, unknown>).claim === "string" && typeof (item as Record<string, unknown>).evidence === "string")) throw new Error("invalid shape");
|
|
62
|
+
return { verdict: value.verdict as AdvisorVerdict, criticalFindings: value.criticalFindings as Advice["criticalFindings"], missingEvidence: value.missingEvidence as string[], smallestNextStep: value.smallestNextStep, verificationRequired: value.verificationRequired as string[], escalationReason: typeof value.escalationReason === "string" ? value.escalationReason : null };
|
|
63
|
+
} catch {
|
|
64
|
+
return { verdict: "insufficient-evidence", criticalFindings: [{ severity: "medium", claim: "Advisor response was not structured", evidence: "The response could not be parsed as the required JSON." }], missingEvidence: [], smallestNextStep: "Request a structured Advisor review before relying on this advice.", verificationRequired: [], escalationReason: null };
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const adviceForText = (advice: Advice) => [
|
|
69
|
+
`**Verdict: ${advice.verdict}**`,
|
|
70
|
+
...advice.criticalFindings.map((finding) => `- **${finding.severity}:** ${finding.claim}${finding.evidence ? ` — ${finding.evidence}` : ""}`),
|
|
71
|
+
advice.missingEvidence.length ? `\n**Missing evidence**\n${advice.missingEvidence.map((item) => `- ${item}`).join("\n")}` : "",
|
|
72
|
+
`\n**Smallest next step**\n${advice.smallestNextStep}`,
|
|
73
|
+
advice.verificationRequired.length ? `\n**Required verification**\n${advice.verificationRequired.map((item) => `- ${item}`).join("\n")}` : "",
|
|
74
|
+
].filter(Boolean).join("\n");
|
|
75
|
+
|
|
51
76
|
export const consult = async (
|
|
52
77
|
ctx: ExtensionContext,
|
|
53
78
|
question?: string,
|
|
@@ -99,17 +124,102 @@ export const consult = async (
|
|
|
99
124
|
.trim() || responseText;
|
|
100
125
|
|
|
101
126
|
if (!advice) throw new Error("Advisor returned no advice.");
|
|
102
|
-
|
|
127
|
+
const structured = parseAdvice(advice);
|
|
128
|
+
return { advice: adviceForText(structured), thinkingText, structured };
|
|
103
129
|
};
|
|
104
130
|
|
|
105
131
|
export const registerAdvisorTool = (pi: ExtensionAPI) => {
|
|
132
|
+
const session = advisorSessionState;
|
|
133
|
+
|
|
134
|
+
pi.registerMessageRenderer?.("advisor-loop-call", (message, _options, theme) => {
|
|
135
|
+
const details = message.details as { question?: string } | undefined;
|
|
136
|
+
return renderAdvisorCallBox(details?.question, theme);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
pi.on("session_start", () => {
|
|
140
|
+
session.resetTask();
|
|
141
|
+
herdrAdvisorBlock.clear();
|
|
142
|
+
});
|
|
143
|
+
|
|
106
144
|
pi.on("before_agent_start", (_event, ctx) => {
|
|
107
145
|
if (!pi.getActiveTools().includes("ask_advisor")) return;
|
|
108
146
|
loadConfig(ctx);
|
|
109
147
|
const guidelines = advisorInvocationGuidelines();
|
|
148
|
+
const budget = session.remainingAutomaticCalls(advisorMaxCallsPerSessionRef);
|
|
149
|
+
if (budget !== undefined) guidelines.push(`Advisor calls remaining this session: ${budget}.\nReserve calls for material decisions, repeated failures, or final review.`);
|
|
110
150
|
return guidelines.length > 0 ? { systemPrompt: `${ctx.getSystemPrompt()}\n\nAdvisor invocation settings:\n${guidelines.map((rule) => `- ${rule}`).join("\n")}` } : undefined;
|
|
111
151
|
});
|
|
112
152
|
|
|
153
|
+
pi.on("tool_call", async (event, ctx) => {
|
|
154
|
+
if (!pi.getActiveTools().includes("ask_advisor")) return;
|
|
155
|
+
loadConfig(ctx);
|
|
156
|
+
if (event.toolName === "ask_advisor" && !session.canUseAutomaticCall(advisorMaxCallsPerSessionRef)) {
|
|
157
|
+
return { block: true, reason: "Advisor call budget exhausted for this session." };
|
|
158
|
+
}
|
|
159
|
+
if (!advisorAutoLoopGateRef) return;
|
|
160
|
+
if (!session.recordToolCall(event.toolName, event.input, advisorLoopThresholdRef)) return;
|
|
161
|
+
let reason = `Advisor loop gate: ${event.toolName} repeated ${advisorLoopThresholdRef} times without a different tool action.`;
|
|
162
|
+
let blockSession = false;
|
|
163
|
+
if (session.canUseAutomaticCall(advisorMaxCallsPerSessionRef)) {
|
|
164
|
+
session.consumeAutomaticCall();
|
|
165
|
+
herdrAdvisorActivity.start();
|
|
166
|
+
pi.sendMessage({
|
|
167
|
+
customType: "advisor-loop-call",
|
|
168
|
+
content: "Automatic Advisor loop review",
|
|
169
|
+
display: true,
|
|
170
|
+
details: { question: `Loop gate: ${event.toolName} repeated ${advisorLoopThresholdRef} times` },
|
|
171
|
+
}, { deliverAs: "steer" });
|
|
172
|
+
try {
|
|
173
|
+
const { advice, structured } = await consult(ctx, `${reason} Review the repeated actions and recommend the smallest safe next step.`);
|
|
174
|
+
session.recordConsultation("automatic", structured.verdict);
|
|
175
|
+
pi.sendMessage({
|
|
176
|
+
customType: "advisor-manual-result",
|
|
177
|
+
content: `Automatic loop-gate Advisor review:\n\n${advice}`,
|
|
178
|
+
display: true,
|
|
179
|
+
details: { advisor: advisorRef, text: advice },
|
|
180
|
+
}, { deliverAs: "steer" });
|
|
181
|
+
if (structured.verdict === "proceed") {
|
|
182
|
+
session.resetRepetition();
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
blockSession = structured.verdict === "blocked";
|
|
186
|
+
if (blockSession) {
|
|
187
|
+
const escalation = structured.escalationReason || structured.smallestNextStep;
|
|
188
|
+
session.block(escalation);
|
|
189
|
+
herdrAdvisorBlock.set(escalation);
|
|
190
|
+
} else {
|
|
191
|
+
reason = "Advisor loop review delivered. Follow its guidance before retrying this command.";
|
|
192
|
+
}
|
|
193
|
+
} catch (error) {
|
|
194
|
+
session.recordConsultation("automatic");
|
|
195
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
196
|
+
reason = `${reason}\nAdvisor loop review failed: ${message}`;
|
|
197
|
+
session.block(reason);
|
|
198
|
+
herdrAdvisorBlock.set(reason);
|
|
199
|
+
blockSession = true;
|
|
200
|
+
if (ctx.hasUI) ctx.ui.notify(`Advisor loop review failed. Session is blocked: ${message}`, "error");
|
|
201
|
+
} finally {
|
|
202
|
+
herdrAdvisorActivity.finish();
|
|
203
|
+
}
|
|
204
|
+
} else {
|
|
205
|
+
reason = `${reason}\nAdvisor loop review was not run because the session call budget is exhausted.`;
|
|
206
|
+
session.block(reason);
|
|
207
|
+
herdrAdvisorBlock.set(reason);
|
|
208
|
+
blockSession = true;
|
|
209
|
+
if (ctx.hasUI) ctx.ui.notify("Advisor loop review was not run because the session call budget is exhausted. Session is blocked.", "error");
|
|
210
|
+
}
|
|
211
|
+
if (blockSession && advisorBlockOnBlockedRef) ctx.abort();
|
|
212
|
+
return { block: true, reason };
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
pi.on("agent_settled", (_event, ctx) => {
|
|
216
|
+
if (session.blocked || !advisorSessionSummaryRef) return;
|
|
217
|
+
const summary = session.summary(advisorMaxCallsPerSessionRef);
|
|
218
|
+
if (summary && ctx.hasUI) ctx.ui.notify(summary, "info");
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
pi.on("session_shutdown", () => herdrAdvisorBlock.clear());
|
|
222
|
+
|
|
113
223
|
pi.registerTool({
|
|
114
224
|
name: "ask_advisor",
|
|
115
225
|
label: "Ask Advisor",
|
|
@@ -159,16 +269,29 @@ export const registerAdvisorTool = (pi: ExtensionAPI) => {
|
|
|
159
269
|
},
|
|
160
270
|
async execute(_id, params, signal, onUpdate, ctx) {
|
|
161
271
|
const question = resolveAdvisorRequest(params.question);
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
272
|
+
herdrAdvisorActivity.start();
|
|
273
|
+
try {
|
|
274
|
+
session.consumeAutomaticCall();
|
|
275
|
+
const { advice, thinkingText, structured } = await consult(ctx, question, signal, (t, tx) => {
|
|
276
|
+
onUpdate?.({
|
|
277
|
+
content: [{ type: "text", text: tx }],
|
|
278
|
+
details: { thinking: t, text: tx, advisor: advisorRef, question },
|
|
279
|
+
});
|
|
166
280
|
});
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
281
|
+
session.recordConsultation("executor", structured.verdict);
|
|
282
|
+
if (structured.verdict === "blocked") {
|
|
283
|
+
const reason = structured.escalationReason || structured.smallestNextStep;
|
|
284
|
+
session.block(reason);
|
|
285
|
+
herdrAdvisorBlock.set(reason);
|
|
286
|
+
if (advisorBlockOnBlockedRef) ctx.abort();
|
|
287
|
+
}
|
|
288
|
+
return {
|
|
289
|
+
content: [{ type: "text", text: `Advisor (${advisorRef})\n\n${advice}` }],
|
|
290
|
+
details: { thinking: thinkingText, text: advice, advisor: advisorRef, question, verdict: structured.verdict },
|
|
291
|
+
};
|
|
292
|
+
} finally {
|
|
293
|
+
herdrAdvisorActivity.finish();
|
|
294
|
+
}
|
|
172
295
|
},
|
|
173
296
|
});
|
|
174
297
|
};
|
package/src/ui.ts
CHANGED
|
@@ -104,6 +104,11 @@ export type AdvisorSettings = {
|
|
|
104
104
|
completionGate: boolean;
|
|
105
105
|
collapseResponses: boolean;
|
|
106
106
|
customRule?: string;
|
|
107
|
+
blockOnBlocked?: boolean;
|
|
108
|
+
autoLoopGate?: boolean;
|
|
109
|
+
loopThreshold?: number;
|
|
110
|
+
maxCallsPerSession?: number;
|
|
111
|
+
sessionSummary?: boolean;
|
|
107
112
|
};
|
|
108
113
|
|
|
109
114
|
export class AdvisorSettingsSelector implements Component, Focusable {
|
|
@@ -178,9 +183,14 @@ export class AdvisorSettingsSelector implements Component, Focusable {
|
|
|
178
183
|
this.row("Completion gate", onOff(this.settings.completionGate), 4),
|
|
179
184
|
this.row("Collapse long responses", onOff(this.settings.collapseResponses), 5),
|
|
180
185
|
this.row("Custom invocation", this.settings.customRule || "None", 6),
|
|
186
|
+
this.row("Block on critical advice", onOff(this.settings.blockOnBlocked ?? true), 7),
|
|
187
|
+
this.row("Automatic loop gate", onOff(this.settings.autoLoopGate ?? true), 8),
|
|
188
|
+
this.row("Loop threshold", `After ${this.settings.loopThreshold ?? 3} repeats`, 9),
|
|
189
|
+
this.row("Max Advisor calls/session", this.settings.maxCallsPerSession === undefined ? "∞" : String(this.settings.maxCallsPerSession), 10),
|
|
190
|
+
this.row("Session Advisor Summary", onOff(this.settings.sessionSummary ?? true), 11),
|
|
181
191
|
];
|
|
182
192
|
if (this.editingCustom) rows.push(` ${this.customInput.render(Math.max(10, width - 6))[0] || ""}`);
|
|
183
|
-
rows.push(this.row("Save changes", "",
|
|
193
|
+
rows.push(this.row("Save changes", "", 12));
|
|
184
194
|
return [
|
|
185
195
|
theme.fg("accent", theme.bold(" Advisor settings")),
|
|
186
196
|
"",
|
|
@@ -203,7 +213,7 @@ export class AdvisorSettingsSelector implements Component, Focusable {
|
|
|
203
213
|
if (matchesKey(keyData, Key.up)) {
|
|
204
214
|
this.selectedRow = Math.max(0, this.selectedRow - 1);
|
|
205
215
|
} else if (matchesKey(keyData, Key.down)) {
|
|
206
|
-
this.selectedRow = Math.min(
|
|
216
|
+
this.selectedRow = Math.min(12, this.selectedRow + 1);
|
|
207
217
|
} else if (matchesKey(keyData, Key.left)) {
|
|
208
218
|
this.adjust(-1);
|
|
209
219
|
} else if (matchesKey(keyData, Key.right)) {
|
|
@@ -216,7 +226,7 @@ export class AdvisorSettingsSelector implements Component, Focusable {
|
|
|
216
226
|
tui.requestRender();
|
|
217
227
|
return;
|
|
218
228
|
}
|
|
219
|
-
if (this.selectedRow ===
|
|
229
|
+
if (this.selectedRow === 12) return this.options.onSave({ ...this.settings, contextMaxChars: this.currentContext().value, effort: this.options.effortLevels[this.effortIndex] });
|
|
220
230
|
this.adjust(1);
|
|
221
231
|
} else if (matchesKey(keyData, Key.escape)) {
|
|
222
232
|
return this.options.onCancel();
|
|
@@ -232,6 +242,16 @@ export class AdvisorSettingsSelector implements Component, Focusable {
|
|
|
232
242
|
case 3: this.settings.failureGate = !this.settings.failureGate; break;
|
|
233
243
|
case 4: this.settings.completionGate = !this.settings.completionGate; break;
|
|
234
244
|
case 5: this.settings.collapseResponses = !this.settings.collapseResponses; break;
|
|
245
|
+
case 7: this.settings.blockOnBlocked = !(this.settings.blockOnBlocked ?? true); break;
|
|
246
|
+
case 8: this.settings.autoLoopGate = !(this.settings.autoLoopGate ?? true); break;
|
|
247
|
+
case 9: this.settings.loopThreshold = Math.max(2, (this.settings.loopThreshold ?? 3) + direction); break;
|
|
248
|
+
case 10: {
|
|
249
|
+
const values = [undefined, 0, 1, 2, 3, 5, 10, 25, 50];
|
|
250
|
+
const index = Math.max(0, values.indexOf(this.settings.maxCallsPerSession));
|
|
251
|
+
this.settings.maxCallsPerSession = values[Math.max(0, Math.min(values.length - 1, index + direction))];
|
|
252
|
+
break;
|
|
253
|
+
}
|
|
254
|
+
case 11: this.settings.sessionSummary = !(this.settings.sessionSummary ?? true); break;
|
|
235
255
|
}
|
|
236
256
|
}
|
|
237
257
|
}
|