pi-advisor-flow 0.8.2 → 0.8.3-dev.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 +13 -0
- package/dist/index.js +319 -184
- package/extensions/index.ts +12 -3
- package/package.json +4 -4
- package/src/commands/lifecycle.ts +1 -2
- package/src/commands/manual-consultation.ts +3 -3
- package/src/commands/runtime.ts +3 -1
- package/src/commands/types.ts +3 -0
- package/src/herdr-block.ts +15 -3
- package/src/herdr.ts +58 -8
- package/src/jev/client.ts +14 -6
- package/src/redaction.ts +6 -1
- package/src/session-state.ts +30 -4
- package/src/tools/gate-policy.ts +8 -5
- package/src/tools/jev-filter.ts +3 -2
- package/src/tools/jev-turn-gate.ts +45 -29
- package/src/tools/loop-gate.ts +32 -12
- package/src/tools/register-ask-advisor.ts +181 -144
- package/src/tools/register-lifecycle.ts +16 -4
- package/src/tools/registration.ts +4 -0
- package/src/tools/types.ts +5 -0
- package/src/ui/jev-setup-submenu.ts +8 -2
package/extensions/index.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
|
|
3
3
|
import { registerCommands } from "../src/commands/registration.ts";
|
|
4
|
-
import {
|
|
4
|
+
import { getAdvisorSettings } from "../src/config/state.ts";
|
|
5
|
+
import { HerdrAdvisorBlock, herdrAdvisorActivity } from "../src/herdr.ts";
|
|
5
6
|
import { AdvisorSessionState } from "../src/session-state.ts";
|
|
6
7
|
import {
|
|
7
8
|
consultAdvisor as consultAdvisorImplementation,
|
|
@@ -34,13 +35,21 @@ export const runAdvisorGate = (
|
|
|
34
35
|
export default function registerPiAdvisor(pi: ExtensionAPI) {
|
|
35
36
|
const sessionState = new AdvisorSessionState();
|
|
36
37
|
const scoutStatus = new ScoutStatusManager();
|
|
37
|
-
|
|
38
|
-
|
|
38
|
+
const herdrActivity = herdrAdvisorActivity.createScope();
|
|
39
|
+
const herdrBlock = new HerdrAdvisorBlock(
|
|
40
|
+
undefined,
|
|
41
|
+
() => getAdvisorSettings().herdrIntegration,
|
|
42
|
+
(active, label) => {
|
|
43
|
+
pi.events.emit("herdr:blocked", { active, label });
|
|
44
|
+
}
|
|
39
45
|
);
|
|
40
46
|
registerAdvisorTool(pi, sessionState, {
|
|
47
|
+
herdrActivity,
|
|
48
|
+
herdrBlock,
|
|
41
49
|
statusManager: scoutStatus,
|
|
42
50
|
});
|
|
43
51
|
registerCommands(pi, {
|
|
52
|
+
herdrActivity,
|
|
44
53
|
sessionState,
|
|
45
54
|
statusManager: scoutStatus,
|
|
46
55
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-advisor-flow",
|
|
3
|
-
"version": "0.8.2",
|
|
3
|
+
"version": "0.8.3-dev.2",
|
|
4
4
|
"description": "Advanced Executor/Advisor flow for Pi, fully configurable and extendable.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"advisor",
|
|
@@ -55,12 +55,12 @@
|
|
|
55
55
|
"@earendil-works/pi-coding-agent": "0.87.1",
|
|
56
56
|
"@earendil-works/pi-tui": "0.87.1",
|
|
57
57
|
"@oxlint/plugins": "1.85.0",
|
|
58
|
-
"@types/node": "^26.6.
|
|
58
|
+
"@types/node": "^26.6.3",
|
|
59
59
|
"@typesafe-ai/sdk": "^0.6.0",
|
|
60
60
|
"bun-types": "1.4.2",
|
|
61
61
|
"husky": "^9.1.7",
|
|
62
|
-
"knip": "^6.
|
|
63
|
-
"lint-staged": "^17.
|
|
62
|
+
"knip": "^6.38.0",
|
|
63
|
+
"lint-staged": "^17.6.0",
|
|
64
64
|
"oxfmt": "0.70.0",
|
|
65
65
|
"oxlint": "1.85.0",
|
|
66
66
|
"typebox": "^1.3.34",
|
|
@@ -8,7 +8,6 @@ import {
|
|
|
8
8
|
setExecutorRef,
|
|
9
9
|
} from "../config/state.ts";
|
|
10
10
|
import { loadConfig, saveConfig } from "../config/storage.ts";
|
|
11
|
-
import { herdrAdvisorActivity } from "../herdr.ts";
|
|
12
11
|
import { notify } from "./runtime.ts";
|
|
13
12
|
import type { CommandRuntime } from "./types.ts";
|
|
14
13
|
|
|
@@ -84,6 +83,6 @@ export const registerCommandLifecycle = (
|
|
|
84
83
|
runtime.manualConsultations.clear();
|
|
85
84
|
runtime.manualProgressTimers.clear();
|
|
86
85
|
runtime.manualProgress.clear();
|
|
87
|
-
|
|
86
|
+
runtime.herdrActivity.clear();
|
|
88
87
|
});
|
|
89
88
|
};
|
|
@@ -3,7 +3,7 @@ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
|
3
3
|
import { advisorRef } from "../config/state.ts";
|
|
4
4
|
import { isString } from "../content-utils.ts";
|
|
5
5
|
import type { GitContextLevel } from "../git.ts";
|
|
6
|
-
import {
|
|
6
|
+
import { notifyHerdrAdvisorFailure } from "../herdr.ts";
|
|
7
7
|
import { normalizeScreeningQuestion } from "../tools/jev-filter.ts";
|
|
8
8
|
import { appendScoutLifecycleEntry } from "../tools/scout-status.ts";
|
|
9
9
|
import type { ScoutToolDetails } from "../tools/types.ts";
|
|
@@ -28,7 +28,6 @@ export const startManualConsultation = async (
|
|
|
28
28
|
progress: ManualAdvisorProgressState,
|
|
29
29
|
gitContext?: GitContextLevel
|
|
30
30
|
) => {
|
|
31
|
-
herdrAdvisorActivity.start();
|
|
32
31
|
progress.phase = "preparing";
|
|
33
32
|
runtime.requestManualRender(ctx);
|
|
34
33
|
if (ctx.hasUI) {
|
|
@@ -44,6 +43,7 @@ export const startManualConsultation = async (
|
|
|
44
43
|
}, 80);
|
|
45
44
|
runtime.manualProgressTimers.set(controller, timer);
|
|
46
45
|
}
|
|
46
|
+
const finishHerdrActivity = runtime.herdrActivity.start();
|
|
47
47
|
let scoutDetails: ScoutToolDetails | undefined;
|
|
48
48
|
try {
|
|
49
49
|
const { adviceId, markdown, usage } = await runtime.requestAdvisor(
|
|
@@ -152,6 +152,7 @@ export const startManualConsultation = async (
|
|
|
152
152
|
notify(ctx, `Advisor consultation failed: ${message}`, "error");
|
|
153
153
|
notifyHerdrAdvisorFailure("Advisor consultation failed", message);
|
|
154
154
|
} finally {
|
|
155
|
+
finishHerdrActivity();
|
|
155
156
|
if (controller.signal.aborted) {
|
|
156
157
|
progress.phase = "cancelled";
|
|
157
158
|
}
|
|
@@ -163,6 +164,5 @@ export const startManualConsultation = async (
|
|
|
163
164
|
runtime.requestManualRender(ctx);
|
|
164
165
|
runtime.scoutStatus.release(ctx, scoutStatusToken);
|
|
165
166
|
runtime.manualConsultations.delete(controller);
|
|
166
|
-
herdrAdvisorActivity.finish();
|
|
167
167
|
}
|
|
168
168
|
};
|
package/src/commands/runtime.ts
CHANGED
|
@@ -4,7 +4,7 @@ import type {
|
|
|
4
4
|
} from "@earendil-works/pi-coding-agent";
|
|
5
5
|
|
|
6
6
|
import { getAdvisorSettings } from "../config/state.ts";
|
|
7
|
-
import { notifyHerdrAdvisorFailure } from "../herdr.ts";
|
|
7
|
+
import { herdrAdvisorActivity, notifyHerdrAdvisorFailure } from "../herdr.ts";
|
|
8
8
|
import type { AdvisorSessionState } from "../session-state.ts";
|
|
9
9
|
import { consultAdvisor } from "../tools/consultation.ts";
|
|
10
10
|
import { ScoutStatusManager } from "../tools/scout-status.ts";
|
|
@@ -40,6 +40,7 @@ const requestManualRender = (ctx: ExtensionContext) => {
|
|
|
40
40
|
|
|
41
41
|
class CommandRuntime implements CommandRuntimeContract {
|
|
42
42
|
readonly advisorSessionState: AdvisorSessionState;
|
|
43
|
+
readonly herdrActivity: NonNullable<CommandDependencies["herdrActivity"]>;
|
|
43
44
|
readonly manualConsultations = new Map<AbortController, symbol>();
|
|
44
45
|
readonly manualProgress = new Map<string, ManualAdvisorProgressState>();
|
|
45
46
|
readonly manualProgressTimers = new Map<
|
|
@@ -59,6 +60,7 @@ class CommandRuntime implements CommandRuntimeContract {
|
|
|
59
60
|
this.pi = pi;
|
|
60
61
|
this.advisorSessionState =
|
|
61
62
|
dependencies.sessionState ?? defaultAdvisorSessionState;
|
|
63
|
+
this.herdrActivity = dependencies.herdrActivity ?? herdrAdvisorActivity;
|
|
62
64
|
this.scoutStatus =
|
|
63
65
|
dependencies.statusManager ?? new ScoutStatusManager(false);
|
|
64
66
|
this.requestAdvisor =
|
package/src/commands/types.ts
CHANGED
|
@@ -4,6 +4,7 @@ import type {
|
|
|
4
4
|
} from "@earendil-works/pi-coding-agent";
|
|
5
5
|
|
|
6
6
|
import type { GitContextLevel } from "../git.ts";
|
|
7
|
+
import type { HerdrAdvisorActivityScope } from "../herdr.ts";
|
|
7
8
|
import type { ScoutLifecycleEvent } from "../scout.ts";
|
|
8
9
|
import type { AdvisorSessionState } from "../session-state.ts";
|
|
9
10
|
import type { ScoutStatusManager } from "../tools/scout-status.ts";
|
|
@@ -43,12 +44,14 @@ export interface ManualAdvisorProgressState {
|
|
|
43
44
|
|
|
44
45
|
export interface CommandDependencies {
|
|
45
46
|
consult?: ManualConsult;
|
|
47
|
+
herdrActivity?: HerdrAdvisorActivityScope;
|
|
46
48
|
sessionState?: AdvisorSessionState;
|
|
47
49
|
statusManager?: ScoutStatusManager;
|
|
48
50
|
}
|
|
49
51
|
|
|
50
52
|
export interface CommandRuntime {
|
|
51
53
|
readonly advisorSessionState: AdvisorSessionState;
|
|
54
|
+
readonly herdrActivity: HerdrAdvisorActivityScope;
|
|
52
55
|
flowEnabled: () => boolean;
|
|
53
56
|
readonly manualConsultations: Map<AbortController, symbol>;
|
|
54
57
|
readonly manualProgress: Map<string, ManualAdvisorProgressState>;
|
package/src/herdr-block.ts
CHANGED
|
@@ -10,13 +10,16 @@ import {
|
|
|
10
10
|
|
|
11
11
|
export class HerdrAdvisorBlock {
|
|
12
12
|
#blocked = false;
|
|
13
|
+
private readonly emitBlocked: (active: boolean, label: string) => void;
|
|
13
14
|
private readonly report: Report;
|
|
14
15
|
private readonly enabled: () => boolean;
|
|
15
16
|
|
|
16
17
|
constructor(
|
|
17
18
|
report: Report = sendToHerdr,
|
|
18
|
-
enabled: () => boolean = () => true
|
|
19
|
+
enabled: () => boolean = () => true,
|
|
20
|
+
emitBlocked: (active: boolean, label: string) => void = safeEmitBlocked
|
|
19
21
|
) {
|
|
22
|
+
this.emitBlocked = emitBlocked;
|
|
20
23
|
this.report = report;
|
|
21
24
|
this.enabled = enabled;
|
|
22
25
|
}
|
|
@@ -29,7 +32,7 @@ export class HerdrAdvisorBlock {
|
|
|
29
32
|
const wasBlocked: boolean = this.#blocked;
|
|
30
33
|
if (!wasBlocked) {
|
|
31
34
|
// The listener refcounts, so only the false → true edge may emit.
|
|
32
|
-
safeEmitBlocked(true, label);
|
|
35
|
+
this.safeEmitBlocked(true, label);
|
|
33
36
|
}
|
|
34
37
|
this.#blocked = true;
|
|
35
38
|
this.safeReport({ blocked: label });
|
|
@@ -43,7 +46,7 @@ export class HerdrAdvisorBlock {
|
|
|
43
46
|
}
|
|
44
47
|
// Clearing previously reported state is a de-escalation and must still be
|
|
45
48
|
// delivered if integration was disabled after the block was reported.
|
|
46
|
-
safeEmitBlocked(false);
|
|
49
|
+
this.safeEmitBlocked(false, "Advisor blocked");
|
|
47
50
|
try {
|
|
48
51
|
this.report({
|
|
49
52
|
id: `${BLOCK_SOURCE}:${nextSequence()}`,
|
|
@@ -62,6 +65,15 @@ export class HerdrAdvisorBlock {
|
|
|
62
65
|
}
|
|
63
66
|
}
|
|
64
67
|
|
|
68
|
+
private safeEmitBlocked(active: boolean, label: string): boolean {
|
|
69
|
+
try {
|
|
70
|
+
this.emitBlocked(active, label);
|
|
71
|
+
return true;
|
|
72
|
+
} catch {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
65
77
|
private safeReport(labels: { blocked: string }) {
|
|
66
78
|
try {
|
|
67
79
|
this.report({
|
package/src/herdr.ts
CHANGED
|
@@ -32,8 +32,15 @@ const metadataRequest = (clear: boolean): HerdrMetadataRequest => ({
|
|
|
32
32
|
},
|
|
33
33
|
});
|
|
34
34
|
|
|
35
|
-
export
|
|
35
|
+
export interface HerdrAdvisorActivityScope {
|
|
36
|
+
clear: () => void;
|
|
37
|
+
start: () => () => void;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export class HerdrAdvisorActivity implements HerdrAdvisorActivityScope {
|
|
36
41
|
#activeConsultations = 0;
|
|
42
|
+
#activeByOwner = new Map<symbol, Set<symbol>>();
|
|
43
|
+
#defaultOwner = Symbol("default Herdr Advisor activity");
|
|
37
44
|
private readonly report: Report;
|
|
38
45
|
private readonly enabled: () => boolean;
|
|
39
46
|
|
|
@@ -46,31 +53,74 @@ export class HerdrAdvisorActivity {
|
|
|
46
53
|
}
|
|
47
54
|
|
|
48
55
|
start() {
|
|
56
|
+
return this.startFor(this.#defaultOwner);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
finish() {
|
|
60
|
+
this.finishFor(this.#defaultOwner);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
clear() {
|
|
64
|
+
this.clearFor(this.#defaultOwner);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
createScope(): HerdrAdvisorActivityScope {
|
|
68
|
+
const owner = Symbol("Herdr Advisor runtime");
|
|
69
|
+
return {
|
|
70
|
+
clear: () => this.clearFor(owner),
|
|
71
|
+
start: () => this.startFor(owner),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
private startFor(owner: symbol) {
|
|
76
|
+
const lease = Symbol("Herdr activity lease");
|
|
49
77
|
if (!this.enabled()) {
|
|
50
|
-
return;
|
|
78
|
+
return () => this.finishLease(owner, lease);
|
|
51
79
|
}
|
|
80
|
+
let leases = this.#activeByOwner.get(owner);
|
|
81
|
+
if (!leases) {
|
|
82
|
+
leases = new Set();
|
|
83
|
+
this.#activeByOwner.set(owner, leases);
|
|
84
|
+
}
|
|
85
|
+
leases.add(lease);
|
|
52
86
|
this.#activeConsultations += 1;
|
|
53
87
|
if (this.#activeConsultations === 1) {
|
|
54
88
|
this.safeReport(false);
|
|
55
89
|
}
|
|
90
|
+
return () => this.finishLease(owner, lease);
|
|
56
91
|
}
|
|
57
92
|
|
|
58
|
-
|
|
59
|
-
|
|
93
|
+
private finishFor(owner: symbol) {
|
|
94
|
+
const lease = this.#activeByOwner.get(owner)?.values().next().value;
|
|
95
|
+
if (lease) {
|
|
96
|
+
this.finishLease(owner, lease);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
private finishLease(owner: symbol, lease: symbol) {
|
|
101
|
+
const leases = this.#activeByOwner.get(owner);
|
|
102
|
+
if (!leases?.delete(lease)) {
|
|
60
103
|
return;
|
|
61
104
|
}
|
|
105
|
+
if (leases.size === 0) {
|
|
106
|
+
this.#activeByOwner.delete(owner);
|
|
107
|
+
}
|
|
62
108
|
this.#activeConsultations -= 1;
|
|
63
109
|
if (this.#activeConsultations === 0) {
|
|
64
110
|
this.safeReport(true);
|
|
65
111
|
}
|
|
66
112
|
}
|
|
67
113
|
|
|
68
|
-
|
|
69
|
-
|
|
114
|
+
private clearFor(owner: symbol) {
|
|
115
|
+
const leases = this.#activeByOwner.get(owner);
|
|
116
|
+
if (!leases?.size) {
|
|
70
117
|
return;
|
|
71
118
|
}
|
|
72
|
-
this.#
|
|
73
|
-
this.
|
|
119
|
+
this.#activeByOwner.delete(owner);
|
|
120
|
+
this.#activeConsultations -= leases.size;
|
|
121
|
+
if (this.#activeConsultations === 0) {
|
|
122
|
+
this.safeReport(true);
|
|
123
|
+
}
|
|
74
124
|
}
|
|
75
125
|
|
|
76
126
|
private safeReport(clear: boolean) {
|
package/src/jev/client.ts
CHANGED
|
@@ -97,9 +97,12 @@ const connectionFailure = <E>(error: E) => {
|
|
|
97
97
|
};
|
|
98
98
|
};
|
|
99
99
|
|
|
100
|
-
const sleepWithAbort = (signal: AbortSignal, ms: number): Promise<void> =>
|
|
100
|
+
const sleepWithAbort = (signal: AbortSignal, ms: number): Promise<void> => {
|
|
101
|
+
if (signal.aborted) {
|
|
102
|
+
return Promise.resolve();
|
|
103
|
+
}
|
|
101
104
|
// oxlint-disable-next-line promise/avoid-new -- unref-ed abortable retry timer cannot be expressed with async/await.
|
|
102
|
-
new Promise((resolve) => {
|
|
105
|
+
return new Promise((resolve) => {
|
|
103
106
|
const timer = setTimeout(resolve, ms);
|
|
104
107
|
timer.unref?.();
|
|
105
108
|
signal.addEventListener(
|
|
@@ -111,6 +114,7 @@ const sleepWithAbort = (signal: AbortSignal, ms: number): Promise<void> =>
|
|
|
111
114
|
{ once: true }
|
|
112
115
|
);
|
|
113
116
|
});
|
|
117
|
+
};
|
|
114
118
|
|
|
115
119
|
/** One systemone client for both transports with a total wall deadline so
|
|
116
120
|
* retries can never stall a tool call; errors are classified and redacted on
|
|
@@ -167,7 +171,11 @@ export class JevClient {
|
|
|
167
171
|
for (let attempt = 1; ; attempt += 1) {
|
|
168
172
|
// The retry loop is bounded to one backoff by the deadline controller.
|
|
169
173
|
outcome = await this.#attempt(body, deadline.signal);
|
|
170
|
-
if (
|
|
174
|
+
if (
|
|
175
|
+
!outcome.retryable ||
|
|
176
|
+
attempt >= MAX_ATTEMPTS ||
|
|
177
|
+
deadline.signal.aborted
|
|
178
|
+
) {
|
|
171
179
|
break;
|
|
172
180
|
}
|
|
173
181
|
await sleepWithAbort(deadline.signal, RETRY_BACKOFF_MS);
|
|
@@ -212,12 +220,12 @@ export class JevClient {
|
|
|
212
220
|
`Jev call exceeded its ${this.#timeoutMs} ms wall-time budget.`
|
|
213
221
|
);
|
|
214
222
|
}
|
|
215
|
-
if (outcome.failure) {
|
|
216
|
-
throw outcome.failure;
|
|
217
|
-
}
|
|
218
223
|
if (signal?.aborted) {
|
|
219
224
|
throw new Error("Jev call aborted by the caller.");
|
|
220
225
|
}
|
|
226
|
+
if (outcome.failure) {
|
|
227
|
+
throw outcome.failure;
|
|
228
|
+
}
|
|
221
229
|
throw new JevFailureError("error", "Jev call failed.");
|
|
222
230
|
}
|
|
223
231
|
|
package/src/redaction.ts
CHANGED
|
@@ -3,10 +3,15 @@ import { capUtf8Bytes, isString } from "./content-utils.ts";
|
|
|
3
3
|
const REDACTION_MARKER = "[REDACTED SECRET]";
|
|
4
4
|
const PEM_BEGIN_PATTERN = /-----BEGIN(?: [A-Z0-9]+)? PRIVATE KEY-----/giu;
|
|
5
5
|
const PEM_END_PATTERN = /-----END(?: [A-Z0-9]+)? PRIVATE KEY-----/iu;
|
|
6
|
+
const CREDENTIAL_NAME = String.raw`(?:api[_-]?key|token|secret|password|passwd)`;
|
|
7
|
+
const SECRET_ASSIGNMENT_PATTERN = new RegExp(
|
|
8
|
+
String.raw`(?:(?:"(?:[a-z0-9]+[_-])*?${CREDENTIAL_NAME}"|'(?:[a-z0-9]+[_-])*?${CREDENTIAL_NAME}')|(?:\b|_)${CREDENTIAL_NAME})\s*[:=]\s*(?:"(?:\\.|[^"])*(?:"|$)|'(?:\\.|[^'])*(?:'|$)|[^\s"'&,;)}\]]+)`,
|
|
9
|
+
"giu"
|
|
10
|
+
);
|
|
6
11
|
const SECRET_PATTERNS = [
|
|
7
12
|
/-----BEGIN(?: [A-Z0-9]+)? PRIVATE KEY-----[\s\S]*?-----END(?: [A-Z0-9]+)? PRIVATE KEY-----/giu,
|
|
8
13
|
/\bBearer\s+[A-Za-z0-9._~+/=-]{8,}/giu,
|
|
9
|
-
|
|
14
|
+
SECRET_ASSIGNMENT_PATTERN,
|
|
10
15
|
/(?<scheme>[a-z][a-z0-9+.-]*:\/\/)[^\s/@:]+:[^\s/@]+@/giu,
|
|
11
16
|
/\b(?:AKIA|ASIA)[A-Z0-9]{16}\b/gu,
|
|
12
17
|
/\b(?:aws_secret_access_key|aws_session_token)\s*[:=]\s*[^\s"'&,;)}\]]+/giu,
|
package/src/session-state.ts
CHANGED
|
@@ -177,6 +177,7 @@ export class AdvisorSessionState {
|
|
|
177
177
|
#ledger = freshAdviceLedger();
|
|
178
178
|
#usage = freshUsage();
|
|
179
179
|
#consumedCalls = 0;
|
|
180
|
+
#callReservations = new Set<string>();
|
|
180
181
|
readonly #jev = new AdvisorJevLedgerState();
|
|
181
182
|
#sessionTurnOrdinal = 0;
|
|
182
183
|
#turnsSinceConsultation = 0;
|
|
@@ -187,6 +188,7 @@ export class AdvisorSessionState {
|
|
|
187
188
|
this.#ledger = freshAdviceLedger();
|
|
188
189
|
this.#usage = freshUsage();
|
|
189
190
|
this.#consumedCalls = 0;
|
|
191
|
+
this.#callReservations.clear();
|
|
190
192
|
this.#jev.reset();
|
|
191
193
|
this.#sessionTurnOrdinal = 0;
|
|
192
194
|
this.#turnsSinceConsultation = 0;
|
|
@@ -227,16 +229,40 @@ export class AdvisorSessionState {
|
|
|
227
229
|
return true;
|
|
228
230
|
}
|
|
229
231
|
|
|
230
|
-
canConsult(limit: number | undefined) {
|
|
231
|
-
|
|
232
|
+
canConsult(limit: number | undefined, reservationId?: string) {
|
|
233
|
+
const reservations =
|
|
234
|
+
this.#callReservations.size -
|
|
235
|
+
Number(
|
|
236
|
+
reservationId !== undefined && this.#callReservations.has(reservationId)
|
|
237
|
+
);
|
|
238
|
+
return limit === undefined || this.#consumedCalls + reservations < limit;
|
|
232
239
|
}
|
|
233
|
-
|
|
240
|
+
reserveCall(id: string, limit: number | undefined) {
|
|
241
|
+
if (this.#callReservations.has(id)) {
|
|
242
|
+
return true;
|
|
243
|
+
}
|
|
244
|
+
if (!this.canConsult(limit)) {
|
|
245
|
+
return false;
|
|
246
|
+
}
|
|
247
|
+
this.#callReservations.add(id);
|
|
248
|
+
return true;
|
|
249
|
+
}
|
|
250
|
+
releaseCall(id: string) {
|
|
251
|
+
this.#callReservations.delete(id);
|
|
252
|
+
}
|
|
253
|
+
clearCallReservations() {
|
|
254
|
+
this.#callReservations.clear();
|
|
255
|
+
}
|
|
256
|
+
consumeCall(reservationId?: string) {
|
|
257
|
+
if (reservationId !== undefined) {
|
|
258
|
+
this.#callReservations.delete(reservationId);
|
|
259
|
+
}
|
|
234
260
|
this.#consumedCalls += 1;
|
|
235
261
|
}
|
|
236
262
|
remainingCalls(limit: number | undefined) {
|
|
237
263
|
return limit === undefined
|
|
238
264
|
? undefined
|
|
239
|
-
: Math.max(0, limit - this.#consumedCalls);
|
|
265
|
+
: Math.max(0, limit - this.#consumedCalls - this.#callReservations.size);
|
|
240
266
|
}
|
|
241
267
|
get consumedCalls() {
|
|
242
268
|
return this.#consumedCalls;
|
package/src/tools/gate-policy.ts
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
isSimpleMode,
|
|
12
12
|
} from "../config/state.ts";
|
|
13
13
|
import type { GateFailureMode } from "../config/types.ts";
|
|
14
|
+
import type { HerdrAdvisorBlock } from "../herdr-block.ts";
|
|
14
15
|
import { herdrAdvisorBlock, notifyHerdrAdvisorFailure } from "../herdr.ts";
|
|
15
16
|
import type { AdvisorSessionState } from "../session-state.ts";
|
|
16
17
|
import type { AdvisorGateResult, GateFailureCategory } from "./types.ts";
|
|
@@ -66,7 +67,8 @@ export const failureEffect = (
|
|
|
66
67
|
message: string,
|
|
67
68
|
ctx: ExtensionContext,
|
|
68
69
|
session: AdvisorSessionState,
|
|
69
|
-
failureMode: GateFailureMode
|
|
70
|
+
failureMode: GateFailureMode,
|
|
71
|
+
herdrBlock: HerdrAdvisorBlock = herdrAdvisorBlock
|
|
70
72
|
) => {
|
|
71
73
|
const reason = `Advisor gate ${category}: ${message}`;
|
|
72
74
|
notifyLocalFailure(ctx, message, failureMode === "block-session");
|
|
@@ -78,7 +80,7 @@ export const failureEffect = (
|
|
|
78
80
|
return { block: true, effect: "tool-blocked" as const, reason };
|
|
79
81
|
}
|
|
80
82
|
session.block(reason);
|
|
81
|
-
|
|
83
|
+
herdrBlock.set(reason);
|
|
82
84
|
if (advisorBlockOnBlockedRef) {
|
|
83
85
|
ctx.abort();
|
|
84
86
|
}
|
|
@@ -89,7 +91,8 @@ export const blockedDecisionEffect = (
|
|
|
89
91
|
reason: string,
|
|
90
92
|
ctx: ExtensionContext,
|
|
91
93
|
session: AdvisorSessionState,
|
|
92
|
-
failureMode: GateFailureMode
|
|
94
|
+
failureMode: GateFailureMode,
|
|
95
|
+
herdrBlock: HerdrAdvisorBlock = herdrAdvisorBlock
|
|
93
96
|
) => {
|
|
94
97
|
if (failureMode === "warn-and-continue") {
|
|
95
98
|
if (ctx.hasUI) {
|
|
@@ -104,7 +107,7 @@ export const blockedDecisionEffect = (
|
|
|
104
107
|
return { block: true, effect: "tool-blocked" as const, reason };
|
|
105
108
|
}
|
|
106
109
|
session.block(reason);
|
|
107
|
-
|
|
110
|
+
herdrBlock.set(reason);
|
|
108
111
|
if (advisorBlockOnBlockedRef) {
|
|
109
112
|
ctx.abort();
|
|
110
113
|
}
|
|
@@ -120,7 +123,7 @@ export const reserveAdvisorCall = (
|
|
|
120
123
|
if (event.toolName !== "ask_advisor" || isSimpleMode()) {
|
|
121
124
|
return;
|
|
122
125
|
}
|
|
123
|
-
if (!session.
|
|
126
|
+
if (!session.reserveCall(event.toolCallId, getAdvisorMaxCallsPerSession())) {
|
|
124
127
|
const message = "Advisor call budget exhausted for this session.";
|
|
125
128
|
if (ctx.hasUI) {
|
|
126
129
|
ctx.ui.notify(message, "warning");
|
package/src/tools/jev-filter.ts
CHANGED
|
@@ -140,11 +140,12 @@ const screenWithJev = async (
|
|
|
140
140
|
session.recordJevFilterAllowed();
|
|
141
141
|
return allow();
|
|
142
142
|
} catch (error) {
|
|
143
|
+
if (options.signal?.aborted) {
|
|
144
|
+
throw error;
|
|
145
|
+
}
|
|
143
146
|
session.recordJevFilterFailure();
|
|
144
147
|
if (error instanceof JevFailure) {
|
|
145
148
|
notifyOutageOnce(ctx, error.category, error.message);
|
|
146
|
-
} else if (options.signal?.aborted) {
|
|
147
|
-
throw error;
|
|
148
149
|
} else {
|
|
149
150
|
notifyOutageOnce(
|
|
150
151
|
ctx,
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
isSimpleMode,
|
|
12
12
|
} from "../config/state.ts";
|
|
13
13
|
import { herdrAdvisorActivity } from "../herdr.ts";
|
|
14
|
+
import type { HerdrAdvisorActivityScope } from "../herdr.ts";
|
|
14
15
|
import { JevFailure, jevClientFromCredentials } from "../jev/client.ts";
|
|
15
16
|
import { composeTurnGateVerdict } from "../jev/questions.ts";
|
|
16
17
|
import { buildJevState } from "../jev/state.ts";
|
|
@@ -55,6 +56,7 @@ export interface JevTurnGateRegistration {
|
|
|
55
56
|
activeTools: () => string[];
|
|
56
57
|
consult: typeof consultAdvisor;
|
|
57
58
|
deps?: JevTurnGateDeps;
|
|
59
|
+
herdrActivity?: HerdrAdvisorActivityScope;
|
|
58
60
|
send: (message: {
|
|
59
61
|
content: string;
|
|
60
62
|
customType: string;
|
|
@@ -71,6 +73,35 @@ const outageNotifier = createOutageNotifier(
|
|
|
71
73
|
|
|
72
74
|
const notifyFailureOnce = outageNotifier.notify;
|
|
73
75
|
|
|
76
|
+
const shouldRunTurnGate = (
|
|
77
|
+
registration: JevTurnGateRegistration,
|
|
78
|
+
ctx: ExtensionContext,
|
|
79
|
+
session: AdvisorSessionState
|
|
80
|
+
) => {
|
|
81
|
+
const interval = advisorJevTurnGateEveryTurnsRef;
|
|
82
|
+
return (
|
|
83
|
+
interval > 0 &&
|
|
84
|
+
session.turnsSinceConsultation > 0 &&
|
|
85
|
+
session.turnsSinceConsultation % interval === 0 &&
|
|
86
|
+
!isSimpleMode() &&
|
|
87
|
+
!session.blocked &&
|
|
88
|
+
registration.activeTools().includes("ask_advisor") &&
|
|
89
|
+
advisorModelIsAllowed(ctx) &&
|
|
90
|
+
session.canConsult(getAdvisorMaxCallsPerSession())
|
|
91
|
+
);
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const consumeTurnGateBudget = (
|
|
95
|
+
session: AdvisorSessionState,
|
|
96
|
+
shouldConsult: boolean
|
|
97
|
+
) => {
|
|
98
|
+
if (!shouldConsult || !session.canConsult(getAdvisorMaxCallsPerSession())) {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
session.consumeCall();
|
|
102
|
+
return true;
|
|
103
|
+
};
|
|
104
|
+
|
|
74
105
|
/** Test-only: re-arms the once-per-outage notification. */
|
|
75
106
|
export const resetJevTurnGateNotification = outageNotifier.reset;
|
|
76
107
|
|
|
@@ -86,21 +117,7 @@ export const handleJevTurnEnd = async (
|
|
|
86
117
|
): Promise<void> => {
|
|
87
118
|
const { session } = registration;
|
|
88
119
|
session.recordCompletedTurn();
|
|
89
|
-
|
|
90
|
-
if (
|
|
91
|
-
interval <= 0 ||
|
|
92
|
-
session.turnsSinceConsultation <= 0 ||
|
|
93
|
-
session.turnsSinceConsultation % interval !== 0
|
|
94
|
-
) {
|
|
95
|
-
return;
|
|
96
|
-
}
|
|
97
|
-
if (
|
|
98
|
-
isSimpleMode() ||
|
|
99
|
-
session.blocked ||
|
|
100
|
-
!registration.activeTools().includes("ask_advisor") ||
|
|
101
|
-
!advisorModelIsAllowed(ctx) ||
|
|
102
|
-
!session.canConsult(getAdvisorMaxCallsPerSession())
|
|
103
|
-
) {
|
|
120
|
+
if (!shouldRunTurnGate(registration, ctx, session)) {
|
|
104
121
|
return;
|
|
105
122
|
}
|
|
106
123
|
|
|
@@ -128,22 +145,21 @@ export const handleJevTurnEnd = async (
|
|
|
128
145
|
result.answers,
|
|
129
146
|
advisorJevTurnGateNoulThresholdRef
|
|
130
147
|
);
|
|
131
|
-
if (!shouldConsult) {
|
|
148
|
+
if (!consumeTurnGateBudget(session, shouldConsult)) {
|
|
132
149
|
return;
|
|
133
150
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
herdrAdvisorActivity.start();
|
|
137
|
-
registration.send({
|
|
138
|
-
content: "Proactive Advisor turn review",
|
|
139
|
-
customType: "advisor-turn-gate-call",
|
|
140
|
-
details: {
|
|
141
|
-
question: `Turn gate: ${session.turnsSinceConsultation} turns without a consultation`,
|
|
142
|
-
turn: session.sessionTurnOrdinal,
|
|
143
|
-
},
|
|
144
|
-
display: true,
|
|
145
|
-
});
|
|
151
|
+
const herdrActivity = registration.herdrActivity ?? herdrAdvisorActivity;
|
|
152
|
+
const finishHerdrActivity = herdrActivity.start();
|
|
146
153
|
try {
|
|
154
|
+
registration.send({
|
|
155
|
+
content: "Proactive Advisor turn review",
|
|
156
|
+
customType: "advisor-turn-gate-call",
|
|
157
|
+
details: {
|
|
158
|
+
question: `Turn gate: ${session.turnsSinceConsultation} turns without a consultation`,
|
|
159
|
+
turn: session.sessionTurnOrdinal,
|
|
160
|
+
},
|
|
161
|
+
display: true,
|
|
162
|
+
});
|
|
147
163
|
const consulted = await consult(
|
|
148
164
|
ctx,
|
|
149
165
|
undefined,
|
|
@@ -173,7 +189,7 @@ export const handleJevTurnEnd = async (
|
|
|
173
189
|
display: true,
|
|
174
190
|
});
|
|
175
191
|
} finally {
|
|
176
|
-
|
|
192
|
+
finishHerdrActivity();
|
|
177
193
|
}
|
|
178
194
|
} catch (error) {
|
|
179
195
|
session.recordJevGateFailure();
|