pi-advisor-flow 0.7.0 → 0.8.0
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 +19 -18
- package/README.md +9 -8
- package/dist/index.js +1390 -1068
- package/extensions/index.ts +2 -1
- package/package.json +48 -35
- package/src/attachments.ts +11 -10
- package/src/commands/activation-preparation.ts +1 -0
- package/src/commands/activation.ts +2 -0
- package/src/commands/lifecycle.ts +1 -0
- package/src/commands/manual-command.ts +7 -1
- package/src/commands/manual-consultation.ts +102 -92
- package/src/commands/manual-progress.ts +7 -5
- package/src/commands/model-commands.ts +1 -1
- package/src/commands/model-options.ts +14 -3
- package/src/commands/model-picker.ts +1 -0
- package/src/commands/registration.ts +1 -0
- package/src/commands/renderers.ts +67 -55
- package/src/commands/runtime.ts +16 -14
- package/src/commands/settings-commands.ts +10 -4
- package/src/commands/settings-persistence.ts +16 -2
- package/src/commands/types.ts +1 -0
- package/src/commands.ts +0 -1
- package/src/config/args.ts +1 -1
- package/src/config/defaults.ts +8 -1
- package/src/config/schema.ts +42 -17
- package/src/config/state.ts +12 -3
- package/src/config/storage.ts +34 -23
- package/src/config/types.ts +16 -3
- package/src/config/validation.ts +33 -21
- package/src/config.ts +3 -0
- package/src/content-utils.ts +24 -4
- package/src/conversation.ts +16 -15
- package/src/git.ts +10 -3
- package/src/herdr-block.ts +83 -0
- package/src/herdr-shared.ts +108 -0
- package/src/herdr.ts +30 -192
- package/src/jev/client.ts +75 -85
- package/src/jev/failure.ts +19 -0
- package/src/jev/key-store.ts +26 -11
- package/src/jev/ledger.ts +58 -64
- package/src/jev/questions.ts +26 -23
- package/src/jev/state.ts +15 -12
- package/src/jev/transport.ts +8 -3
- package/src/model-stream.ts +35 -37
- package/src/outcomes.ts +15 -11
- package/src/pi-settings.ts +4 -9
- package/src/preferences.ts +4 -2
- package/src/redaction.ts +10 -12
- package/src/scout-context.ts +59 -60
- package/src/scout-curation.ts +4 -13
- package/src/scout-groups.ts +178 -175
- package/src/scout-protocol.ts +74 -70
- package/src/scout-reconstruct.ts +1 -1
- package/src/scout.ts +115 -73
- package/src/session-state.ts +64 -49
- package/src/tool-result-cap.ts +18 -17
- package/src/tools/consult-context.ts +7 -15
- package/src/tools/consultation.ts +14 -4
- package/src/tools/gate-policy.ts +1 -0
- package/src/tools/gate-protocol.ts +36 -23
- package/src/tools/jev-filter.ts +43 -41
- package/src/tools/jev-turn-gate.ts +29 -9
- package/src/tools/loop-gate.ts +31 -20
- package/src/tools/model-access.ts +46 -0
- package/src/tools/prompts.ts +20 -25
- package/src/tools/register-ask-advisor.ts +36 -16
- package/src/tools/register-lifecycle.ts +22 -0
- package/src/tools/register-outcome.ts +2 -0
- package/src/tools/register-renderers.ts +146 -128
- package/src/tools/registration.ts +12 -14
- package/src/tools/render-advisor-result.ts +35 -22
- package/src/tools/render-common.ts +16 -19
- package/src/tools/scout-status.ts +42 -27
- package/src/tools/types.ts +1 -0
- package/src/tools.ts +0 -1
- package/src/ui/jev-setup-submenu.ts +65 -59
- package/src/ui/manual-dialog-render.ts +1 -0
- package/src/ui/manual-dialog.ts +39 -43
- package/src/ui/masked-input.ts +5 -11
- package/src/ui/model-multi-selector.ts +9 -0
- package/src/ui/model-selector-adapter.ts +31 -0
- package/src/ui/model-selector.ts +14 -161
- package/src/ui/searchable-model-list.ts +230 -0
- package/src/ui/settings-formatting.ts +24 -14
- package/src/ui/settings-items.ts +38 -1
- package/src/ui/settings-list-adapter.ts +15 -12
- package/src/ui/settings-mutations.ts +157 -70
- package/src/ui/settings-selector.ts +18 -9
- package/src/ui/text-setting-submenu.ts +3 -6
- package/src/ui/types.ts +14 -0
- package/src/ui.ts +0 -1
- package/src/usage.ts +26 -19
package/src/tool-result-cap.ts
CHANGED
|
@@ -14,6 +14,24 @@ export interface ToolResultTruncation {
|
|
|
14
14
|
|
|
15
15
|
const OMITTED_MARKER = "[... omitted tool-result section ...]";
|
|
16
16
|
|
|
17
|
+
const collect = (
|
|
18
|
+
candidates: string[],
|
|
19
|
+
maxEntries: number,
|
|
20
|
+
maxContentBytes: number
|
|
21
|
+
) => {
|
|
22
|
+
const selected: string[] = [];
|
|
23
|
+
let used = 0;
|
|
24
|
+
for (const line of candidates.slice(0, maxEntries)) {
|
|
25
|
+
const next = used + byteLength(line) + (selected.length ? 1 : 0);
|
|
26
|
+
if (next > maxContentBytes) {
|
|
27
|
+
break;
|
|
28
|
+
}
|
|
29
|
+
selected.push(line);
|
|
30
|
+
used = next;
|
|
31
|
+
}
|
|
32
|
+
return selected;
|
|
33
|
+
};
|
|
34
|
+
|
|
17
35
|
export const capToolResult = (
|
|
18
36
|
value: string,
|
|
19
37
|
maxLines = DEFAULT_ADVISOR_TOOL_RESULT_MAX_LINES,
|
|
@@ -53,23 +71,6 @@ export const capToolResult = (
|
|
|
53
71
|
}
|
|
54
72
|
const headCount = Math.floor((maxLines - 1) / 2);
|
|
55
73
|
const tailCount = maxLines - 1 - headCount;
|
|
56
|
-
const collect = (
|
|
57
|
-
candidates: string[],
|
|
58
|
-
maxEntries: number,
|
|
59
|
-
maxContentBytes: number
|
|
60
|
-
) => {
|
|
61
|
-
const selected: string[] = [];
|
|
62
|
-
let used = 0;
|
|
63
|
-
for (const line of candidates.slice(0, maxEntries)) {
|
|
64
|
-
const next = used + byteLength(line) + (selected.length ? 1 : 0);
|
|
65
|
-
if (next > maxContentBytes) {
|
|
66
|
-
break;
|
|
67
|
-
}
|
|
68
|
-
selected.push(line);
|
|
69
|
-
used = next;
|
|
70
|
-
}
|
|
71
|
-
return selected;
|
|
72
|
-
};
|
|
73
74
|
const availableBytes = maxBytes - markerBytes - 2;
|
|
74
75
|
const head = collect(lines, headCount, Math.floor(availableBytes / 2));
|
|
75
76
|
const tail = collect(
|
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
type UntrackedAttachment,
|
|
6
|
-
} from "../attachments.ts";
|
|
2
|
+
|
|
3
|
+
import { readTrackedFiles, readUntrackedFiles } from "../attachments.ts";
|
|
4
|
+
import type { UntrackedAttachment } from "../attachments.ts";
|
|
7
5
|
import {
|
|
8
6
|
advisorGitContextMaxCharsRef,
|
|
9
7
|
advisorGitContextRef,
|
|
@@ -13,19 +11,13 @@ import {
|
|
|
13
11
|
advisorUntrackedContentRef,
|
|
14
12
|
contextMaxCharsRef,
|
|
15
13
|
} from "../config/state.ts";
|
|
16
|
-
import {
|
|
17
|
-
|
|
18
|
-
collectGitContext,
|
|
19
|
-
type GitContextLevel,
|
|
20
|
-
} from "../git.ts";
|
|
14
|
+
import { clampGitContextLevel, collectGitContext } from "../git.ts";
|
|
15
|
+
import type { GitContextLevel } from "../git.ts";
|
|
21
16
|
import { readProjectPreferences } from "../preferences.ts";
|
|
22
17
|
import { redactAndCapText, redactSecrets } from "../redaction.ts";
|
|
23
|
-
import {
|
|
24
|
-
runAdvisorScout,
|
|
25
|
-
type ScoutLifecycleEvent,
|
|
26
|
-
type ScoutOutcome,
|
|
27
|
-
} from "../scout.ts";
|
|
28
18
|
import { curateAdvisorConversation } from "../scout-curation.ts";
|
|
19
|
+
import { runAdvisorScout } from "../scout.ts";
|
|
20
|
+
import type { ScoutLifecycleEvent, ScoutOutcome } from "../scout.ts";
|
|
29
21
|
import {
|
|
30
22
|
advisorGitContextBudget,
|
|
31
23
|
advisorRepositoryContext,
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
+
|
|
2
3
|
import type { Message } from "@earendil-works/pi-ai/compat";
|
|
3
4
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
5
|
+
|
|
4
6
|
import {
|
|
5
7
|
advisorEffortRef,
|
|
6
8
|
advisorRedactSecretsRef,
|
|
@@ -14,6 +16,7 @@ import type { ScoutLifecycleEvent } from "../scout.ts";
|
|
|
14
16
|
import type { ConsultationTrigger, GateTrigger } from "../session-state.ts";
|
|
15
17
|
import { assembleConsultationContext } from "./consult-context.ts";
|
|
16
18
|
import { parseAutomaticDecision } from "./gate-protocol.ts";
|
|
19
|
+
import { advisorModelAccessReason } from "./model-access.ts";
|
|
17
20
|
import {
|
|
18
21
|
ADVISOR_DECISION_SYSTEM,
|
|
19
22
|
ADVISOR_SYSTEM,
|
|
@@ -21,7 +24,7 @@ import {
|
|
|
21
24
|
} from "./prompts.ts";
|
|
22
25
|
import type { AdvisorConsultationResult, AdvisorGateOutcome } from "./types.ts";
|
|
23
26
|
|
|
24
|
-
//
|
|
27
|
+
// Re-export preserves the tools facade's historical curation entry point.
|
|
25
28
|
export { curateAdvisorConversation } from "../scout-curation.ts";
|
|
26
29
|
|
|
27
30
|
/** Thrown when the Advisor produced an empty response body. */
|
|
@@ -54,6 +57,10 @@ const collectAdvisorResponse = async (
|
|
|
54
57
|
) => {
|
|
55
58
|
const { ctx, question, signal, systemPrompt } = options;
|
|
56
59
|
loadConfig(ctx);
|
|
60
|
+
const accessReason = advisorModelAccessReason(ctx);
|
|
61
|
+
if (accessReason) {
|
|
62
|
+
throw new Error(accessReason);
|
|
63
|
+
}
|
|
57
64
|
const resolved = await resolveConfiguredModel(ctx, advisorRef, "Advisor");
|
|
58
65
|
const context = await assembleConsultationContext(options);
|
|
59
66
|
|
|
@@ -95,9 +102,9 @@ const collectAdvisorResponse = async (
|
|
|
95
102
|
if (!markdown.trim()) {
|
|
96
103
|
throw new AdvisorNoAdviceError();
|
|
97
104
|
}
|
|
98
|
-
|
|
105
|
+
const response: Omit<AdvisorConsultationResult, "adviceId" | "trigger"> = {
|
|
99
106
|
draftBytes: context.draftText
|
|
100
|
-
? Buffer.byteLength(context.draftText, "
|
|
107
|
+
? Buffer.byteLength(context.draftText, "utf-8")
|
|
101
108
|
: undefined,
|
|
102
109
|
markdown,
|
|
103
110
|
model: advisorRef,
|
|
@@ -108,8 +115,11 @@ const collectAdvisorResponse = async (
|
|
|
108
115
|
untrackedBytes:
|
|
109
116
|
context.untracked.reduce((sum, item) => sum + item.bytes, 0) || undefined,
|
|
110
117
|
usage: streamed.usage,
|
|
111
|
-
...(context.scout ? { scout: context.scout } : {}),
|
|
112
118
|
};
|
|
119
|
+
if (context.scout) {
|
|
120
|
+
response.scout = context.scout;
|
|
121
|
+
}
|
|
122
|
+
return response;
|
|
113
123
|
};
|
|
114
124
|
|
|
115
125
|
export const consultAdvisor = async (
|
package/src/tools/gate-policy.ts
CHANGED
|
@@ -1,20 +1,30 @@
|
|
|
1
1
|
import type { GateDecision } from "../session-state.ts";
|
|
2
2
|
import type { AdvisorGateFailure, AdvisorGateResult } from "./types.ts";
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
const
|
|
6
|
-
|
|
4
|
+
/** Named-group accessor; exec matches of these regexes always populate groups. */
|
|
5
|
+
const namedGroups = (match: RegExpExecArray): Record<string, string> =>
|
|
6
|
+
match.groups ?? {};
|
|
7
|
+
|
|
8
|
+
const DECISION_LINE =
|
|
9
|
+
/^Decision\s*:\s*(?<decision>proceed|revise|blocked)\s*$/iu;
|
|
10
|
+
const CODE_FENCE = /^ {0,3}(?<marker>`{3,}|~{3,})(?<suffix>.*)$/u;
|
|
11
|
+
const LINE_BREAK = /\r?\n/u;
|
|
7
12
|
|
|
8
13
|
interface FenceState {
|
|
9
14
|
character: string;
|
|
10
15
|
length: number;
|
|
11
16
|
}
|
|
12
17
|
|
|
18
|
+
interface FenceAdvance {
|
|
19
|
+
closed: boolean;
|
|
20
|
+
openingFence: FenceState | undefined;
|
|
21
|
+
}
|
|
22
|
+
|
|
13
23
|
const advanceFence = (
|
|
14
24
|
openingFence: FenceState | undefined,
|
|
15
25
|
marker: string,
|
|
16
26
|
suffix: string
|
|
17
|
-
):
|
|
27
|
+
): FenceAdvance => {
|
|
18
28
|
if (!openingFence) {
|
|
19
29
|
if (marker[0] === "`" && suffix.includes("`")) {
|
|
20
30
|
return { closed: false, openingFence: undefined };
|
|
@@ -59,7 +69,8 @@ export const parseAutomaticDecision = (
|
|
|
59
69
|
ok: false,
|
|
60
70
|
};
|
|
61
71
|
}
|
|
62
|
-
|
|
72
|
+
// SAFETY: DECISION_LINE's decision group only matches proceed, revise, or blocked.
|
|
73
|
+
const decision = namedGroups(match).decision.toLowerCase() as GateDecision;
|
|
63
74
|
let openingFence: FenceState | undefined;
|
|
64
75
|
const decisions: string[] = [];
|
|
65
76
|
let pendingFencedDecisions: string[] = [];
|
|
@@ -72,10 +83,11 @@ export const parseAutomaticDecision = (
|
|
|
72
83
|
// open.
|
|
73
84
|
const fence = CODE_FENCE.exec(line);
|
|
74
85
|
if (fence) {
|
|
86
|
+
const groups = namedGroups(fence);
|
|
75
87
|
const { closed, openingFence: nextOpeningFence } = advanceFence(
|
|
76
88
|
openingFence,
|
|
77
|
-
|
|
78
|
-
|
|
89
|
+
groups.marker,
|
|
90
|
+
groups.suffix
|
|
79
91
|
);
|
|
80
92
|
openingFence = nextOpeningFence;
|
|
81
93
|
if (closed) {
|
|
@@ -87,7 +99,7 @@ export const parseAutomaticDecision = (
|
|
|
87
99
|
if (!subsequent) {
|
|
88
100
|
continue;
|
|
89
101
|
}
|
|
90
|
-
const repeated = subsequent
|
|
102
|
+
const repeated = namedGroups(subsequent).decision.trim().toLowerCase();
|
|
91
103
|
if (openingFence) {
|
|
92
104
|
pendingFencedDecisions.push(repeated);
|
|
93
105
|
} else {
|
|
@@ -97,21 +109,22 @@ export const parseAutomaticDecision = (
|
|
|
97
109
|
if (openingFence) {
|
|
98
110
|
decisions.push(...pendingFencedDecisions);
|
|
99
111
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
112
|
+
const [repeated] = decisions;
|
|
113
|
+
if (repeated !== undefined) {
|
|
114
|
+
return repeated === decision
|
|
115
|
+
? {
|
|
116
|
+
category: "duplicate-decision",
|
|
117
|
+
markdown: text,
|
|
118
|
+
message: "Advisor gate response contains duplicate decision lines.",
|
|
119
|
+
ok: false,
|
|
120
|
+
}
|
|
121
|
+
: {
|
|
122
|
+
category: "contradictory-decision",
|
|
123
|
+
markdown: text,
|
|
124
|
+
message:
|
|
125
|
+
"Advisor gate response contains contradictory decision lines.",
|
|
126
|
+
ok: false,
|
|
127
|
+
};
|
|
115
128
|
}
|
|
116
129
|
return {
|
|
117
130
|
decision,
|
package/src/tools/jev-filter.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import type { Fetch } from "@typesafe-ai/sdk";
|
|
3
|
+
|
|
3
4
|
import {
|
|
4
5
|
advisorJevFilterEnabledRef,
|
|
5
6
|
advisorJevFilterNoulMarginRef,
|
|
@@ -14,7 +15,8 @@ import {
|
|
|
14
15
|
screeningQuestions,
|
|
15
16
|
} from "../jev/questions.ts";
|
|
16
17
|
import { buildJevState } from "../jev/state.ts";
|
|
17
|
-
import {
|
|
18
|
+
import { resolveJevTransport } from "../jev/transport.ts";
|
|
19
|
+
import type { JevCredentials } from "../jev/transport.ts";
|
|
18
20
|
import type { AdvisorSessionState } from "../session-state.ts";
|
|
19
21
|
import { createOutageNotifier } from "./outage-notifier.ts";
|
|
20
22
|
|
|
@@ -42,7 +44,7 @@ export interface ScreeningDeps {
|
|
|
42
44
|
}
|
|
43
45
|
|
|
44
46
|
export const normalizeScreeningQuestion = (question: string | undefined) =>
|
|
45
|
-
question?.trim().toLowerCase().
|
|
47
|
+
question?.trim().toLowerCase().replaceAll(/\s+/gu, " ") || undefined;
|
|
46
48
|
|
|
47
49
|
const REATTACHED_ADVICE_CAP_BYTES = 4 * 1024;
|
|
48
50
|
|
|
@@ -64,45 +66,6 @@ export const resetJevOutageNotification = outageNotifier.reset;
|
|
|
64
66
|
|
|
65
67
|
const allow = (): ScreeningOutcome => ({ decision: "allow" });
|
|
66
68
|
|
|
67
|
-
/**
|
|
68
|
-
* Fail-open screening: decides run/skip for one ask_advisor call. Disabled or
|
|
69
|
-
* simple mode, missing credentials, and every Jev failure allow the
|
|
70
|
-
* consultation; only the hard conjunction (negligible-mass ≥ skip confidence
|
|
71
|
-
* AND confidently self-answerable) or an exact repeat skips.
|
|
72
|
-
*/
|
|
73
|
-
export const screenConsultation = (
|
|
74
|
-
ctx: ExtensionContext,
|
|
75
|
-
session: AdvisorSessionState,
|
|
76
|
-
options: ScreenConsultationOptions,
|
|
77
|
-
deps: ScreeningDeps = {}
|
|
78
|
-
): Promise<ScreeningOutcome> => {
|
|
79
|
-
if (isSimpleMode()) {
|
|
80
|
-
return Promise.resolve(allow());
|
|
81
|
-
}
|
|
82
|
-
// Repeat reattachment and its force/passthrough escape hatch are code-side
|
|
83
|
-
// and free: they run regardless of the Jev enable flag, which only gates
|
|
84
|
-
// the (paid, network) Jev screening call.
|
|
85
|
-
const normalizedQuestion = normalizeScreeningQuestion(options.question);
|
|
86
|
-
const bypass = bypassOutcome(session, options, normalizedQuestion);
|
|
87
|
-
if (bypass) {
|
|
88
|
-
return Promise.resolve(bypass);
|
|
89
|
-
}
|
|
90
|
-
const reattached = session.reattachedAdviceFor(normalizedQuestion);
|
|
91
|
-
if (reattached) {
|
|
92
|
-
session.recordJevFilterSkipped(true, normalizedQuestion);
|
|
93
|
-
return Promise.resolve({
|
|
94
|
-
decision: "skip",
|
|
95
|
-
kind: "repeat",
|
|
96
|
-
reason: "already answered earlier in this session",
|
|
97
|
-
reattachedAdvice: reattached.slice(0, REATTACHED_ADVICE_CAP_BYTES),
|
|
98
|
-
});
|
|
99
|
-
}
|
|
100
|
-
if (!advisorJevFilterEnabledRef) {
|
|
101
|
-
return Promise.resolve(allow());
|
|
102
|
-
}
|
|
103
|
-
return screenWithJev(ctx, session, options, deps, normalizedQuestion);
|
|
104
|
-
};
|
|
105
|
-
|
|
106
69
|
const bypassOutcome = (
|
|
107
70
|
session: AdvisorSessionState,
|
|
108
71
|
options: ScreenConsultationOptions,
|
|
@@ -193,6 +156,45 @@ const screenWithJev = async (
|
|
|
193
156
|
}
|
|
194
157
|
};
|
|
195
158
|
|
|
159
|
+
/**
|
|
160
|
+
* Fail-open screening: decides run/skip for one ask_advisor call. Disabled or
|
|
161
|
+
* simple mode, missing credentials, and every Jev failure allow the
|
|
162
|
+
* consultation; only the hard conjunction (negligible-mass ≥ skip confidence
|
|
163
|
+
* AND confidently self-answerable) or an exact repeat skips.
|
|
164
|
+
*/
|
|
165
|
+
export const screenConsultation = (
|
|
166
|
+
ctx: ExtensionContext,
|
|
167
|
+
session: AdvisorSessionState,
|
|
168
|
+
options: ScreenConsultationOptions,
|
|
169
|
+
deps: ScreeningDeps = {}
|
|
170
|
+
): Promise<ScreeningOutcome> => {
|
|
171
|
+
if (isSimpleMode()) {
|
|
172
|
+
return Promise.resolve(allow());
|
|
173
|
+
}
|
|
174
|
+
// Repeat reattachment and its force/passthrough escape hatch are code-side
|
|
175
|
+
// and free: they run regardless of the Jev enable flag, which only gates
|
|
176
|
+
// the (paid, network) Jev screening call.
|
|
177
|
+
const normalizedQuestion = normalizeScreeningQuestion(options.question);
|
|
178
|
+
const bypass = bypassOutcome(session, options, normalizedQuestion);
|
|
179
|
+
if (bypass) {
|
|
180
|
+
return Promise.resolve(bypass);
|
|
181
|
+
}
|
|
182
|
+
const reattached = session.reattachedAdviceFor(normalizedQuestion);
|
|
183
|
+
if (reattached) {
|
|
184
|
+
session.recordJevFilterSkipped(true, normalizedQuestion);
|
|
185
|
+
return Promise.resolve({
|
|
186
|
+
decision: "skip",
|
|
187
|
+
kind: "repeat",
|
|
188
|
+
reason: "already answered earlier in this session",
|
|
189
|
+
reattachedAdvice: reattached.slice(0, REATTACHED_ADVICE_CAP_BYTES),
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
if (!advisorJevFilterEnabledRef) {
|
|
193
|
+
return Promise.resolve(allow());
|
|
194
|
+
}
|
|
195
|
+
return screenWithJev(ctx, session, options, deps, normalizedQuestion);
|
|
196
|
+
};
|
|
197
|
+
|
|
196
198
|
export const screeningSkipText = (outcome: {
|
|
197
199
|
kind: JevSkipKind;
|
|
198
200
|
reattachedAdvice?: string;
|
|
@@ -1,5 +1,9 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
ExtensionContext,
|
|
3
|
+
TurnEndEvent,
|
|
4
|
+
} from "@earendil-works/pi-coding-agent";
|
|
2
5
|
import type { Fetch } from "@typesafe-ai/sdk";
|
|
6
|
+
|
|
3
7
|
import {
|
|
4
8
|
advisorJevTurnGateEveryTurnsRef,
|
|
5
9
|
advisorJevTurnGateNoulThresholdRef,
|
|
@@ -10,11 +14,13 @@ import { herdrAdvisorActivity } from "../herdr.ts";
|
|
|
10
14
|
import { JevFailure, jevClientFromCredentials } from "../jev/client.ts";
|
|
11
15
|
import { composeTurnGateVerdict } from "../jev/questions.ts";
|
|
12
16
|
import { buildJevState } from "../jev/state.ts";
|
|
13
|
-
import {
|
|
17
|
+
import { resolveJevTransport } from "../jev/transport.ts";
|
|
18
|
+
import type { JevCredentials } from "../jev/transport.ts";
|
|
14
19
|
import type { AdvisorSessionState } from "../session-state.ts";
|
|
15
20
|
import { advisorUsageCost } from "../usage.ts";
|
|
16
21
|
import type { consultAdvisor } from "./consultation.ts";
|
|
17
22
|
import { updateAdvisorUsageStatus } from "./gate-policy.ts";
|
|
23
|
+
import { advisorModelIsAllowed } from "./model-access.ts";
|
|
18
24
|
import { createOutageNotifier } from "./outage-notifier.ts";
|
|
19
25
|
|
|
20
26
|
export const turnGateQuestion = {
|
|
@@ -34,6 +40,17 @@ export interface JevTurnGateDeps {
|
|
|
34
40
|
resolveTransport?: () => Promise<JevCredentials | undefined>;
|
|
35
41
|
}
|
|
36
42
|
|
|
43
|
+
export interface TurnGateCallDetails {
|
|
44
|
+
question: string;
|
|
45
|
+
turn: number;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface TurnGateResultDetails {
|
|
49
|
+
advisor: string;
|
|
50
|
+
text: string;
|
|
51
|
+
usage?: unknown;
|
|
52
|
+
}
|
|
53
|
+
|
|
37
54
|
export interface JevTurnGateRegistration {
|
|
38
55
|
activeTools: () => string[];
|
|
39
56
|
consult: typeof consultAdvisor;
|
|
@@ -41,7 +58,7 @@ export interface JevTurnGateRegistration {
|
|
|
41
58
|
send: (message: {
|
|
42
59
|
content: string;
|
|
43
60
|
customType: string;
|
|
44
|
-
details:
|
|
61
|
+
details: TurnGateCallDetails | TurnGateResultDetails;
|
|
45
62
|
display: boolean;
|
|
46
63
|
}) => void;
|
|
47
64
|
session: AdvisorSessionState;
|
|
@@ -81,6 +98,7 @@ export const handleJevTurnEnd = async (
|
|
|
81
98
|
isSimpleMode() ||
|
|
82
99
|
session.blocked ||
|
|
83
100
|
!registration.activeTools().includes("ask_advisor") ||
|
|
101
|
+
!advisorModelIsAllowed(ctx) ||
|
|
84
102
|
!session.canConsult(getAdvisorMaxCallsPerSession())
|
|
85
103
|
) {
|
|
86
104
|
return;
|
|
@@ -171,13 +189,15 @@ export const handleJevTurnEnd = async (
|
|
|
171
189
|
}
|
|
172
190
|
};
|
|
173
191
|
|
|
192
|
+
type TurnEndRegistrar = (
|
|
193
|
+
event: "turn_end",
|
|
194
|
+
handler: (event: TurnEndEvent, ctx: ExtensionContext) => void | Promise<void>
|
|
195
|
+
) => void;
|
|
196
|
+
|
|
174
197
|
/** Wires the turn_end handler on an ExtensionAPI-like surface. */
|
|
175
|
-
export const registerJevTurnGate =
|
|
176
|
-
on:
|
|
177
|
-
event: T,
|
|
178
|
-
handler: (event: unknown, ctx: ExtensionContext) => unknown
|
|
179
|
-
) => void,
|
|
198
|
+
export const registerJevTurnGate = (
|
|
199
|
+
on: TurnEndRegistrar,
|
|
180
200
|
registration: JevTurnGateRegistration
|
|
181
201
|
): void => {
|
|
182
|
-
on("turn_end"
|
|
202
|
+
on("turn_end", (_event, ctx) => handleJevTurnEnd(registration, ctx));
|
|
183
203
|
};
|
package/src/tools/loop-gate.ts
CHANGED
|
@@ -4,6 +4,7 @@ import type {
|
|
|
4
4
|
ToolCallEvent,
|
|
5
5
|
ToolCallEventResult,
|
|
6
6
|
} from "@earendil-works/pi-coding-agent";
|
|
7
|
+
|
|
7
8
|
import {
|
|
8
9
|
advisorAutoLoopGateRef,
|
|
9
10
|
advisorFailureModeRef,
|
|
@@ -15,8 +16,9 @@ import {
|
|
|
15
16
|
} from "../config/state.ts";
|
|
16
17
|
import type { GateFailureMode } from "../config/types.ts";
|
|
17
18
|
import { herdrAdvisorActivity } from "../herdr.ts";
|
|
18
|
-
import type { AdvisorSessionState } from "../session-state.ts";
|
|
19
|
+
import type { AdvisorSessionState, GateDecision } from "../session-state.ts";
|
|
19
20
|
import { advisorUsageCost, snapshotAdvisorUsage } from "../usage.ts";
|
|
21
|
+
import type { AdvisorUsageSnapshot } from "../usage.ts";
|
|
20
22
|
import type { runAdvisorGate } from "./consultation.ts";
|
|
21
23
|
import {
|
|
22
24
|
blockedDecisionEffect,
|
|
@@ -26,10 +28,9 @@ import {
|
|
|
26
28
|
updateAdvisorUsageStatus,
|
|
27
29
|
} from "./gate-policy.ts";
|
|
28
30
|
import { adviceForGateText } from "./gate-protocol.ts";
|
|
29
|
-
import {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
} from "./scout-status.ts";
|
|
31
|
+
import { advisorModelIsAllowed } from "./model-access.ts";
|
|
32
|
+
import { appendScoutLifecycleEntry } from "./scout-status.ts";
|
|
33
|
+
import type { ScoutStatusManager } from "./scout-status.ts";
|
|
33
34
|
import type { AdvisorGateResult, ScoutToolDetails } from "./types.ts";
|
|
34
35
|
|
|
35
36
|
const sendAutomaticGateCall = (pi: ExtensionAPI, event: ToolCallEvent) => {
|
|
@@ -46,20 +47,27 @@ const sendAutomaticGateCall = (pi: ExtensionAPI, event: ToolCallEvent) => {
|
|
|
46
47
|
);
|
|
47
48
|
};
|
|
48
49
|
|
|
50
|
+
interface LoopResultDetails {
|
|
51
|
+
advisor?: string;
|
|
52
|
+
decision?: GateDecision;
|
|
53
|
+
text: string;
|
|
54
|
+
usage?: AdvisorUsageSnapshot;
|
|
55
|
+
}
|
|
56
|
+
|
|
49
57
|
const sendAutomaticGateFailure = (
|
|
50
58
|
pi: ExtensionAPI,
|
|
51
59
|
markdown: string,
|
|
52
|
-
usage?:
|
|
60
|
+
usage?: AdvisorUsageSnapshot
|
|
53
61
|
) => {
|
|
54
|
-
const
|
|
62
|
+
const details: LoopResultDetails = { text: markdown };
|
|
63
|
+
if (usage) {
|
|
64
|
+
details.usage = usage;
|
|
65
|
+
}
|
|
55
66
|
pi.sendMessage(
|
|
56
67
|
{
|
|
57
68
|
content: markdown,
|
|
58
69
|
customType: "advisor-loop-result",
|
|
59
|
-
details
|
|
60
|
-
text: markdown,
|
|
61
|
-
...(normalizedUsage ? { usage: normalizedUsage } : {}),
|
|
62
|
-
},
|
|
70
|
+
details,
|
|
63
71
|
display: true,
|
|
64
72
|
},
|
|
65
73
|
{ deliverAs: "steer" }
|
|
@@ -70,18 +78,20 @@ const sendAutomaticGateResult = (
|
|
|
70
78
|
pi: ExtensionAPI,
|
|
71
79
|
result: AdvisorGateResult
|
|
72
80
|
) => {
|
|
81
|
+
const details: LoopResultDetails = {
|
|
82
|
+
advisor: result.model,
|
|
83
|
+
decision: result.decision,
|
|
84
|
+
text: result.markdown,
|
|
85
|
+
};
|
|
86
|
+
const normalizedUsage = snapshotAdvisorUsage(result.usage);
|
|
87
|
+
if (normalizedUsage) {
|
|
88
|
+
details.usage = normalizedUsage;
|
|
89
|
+
}
|
|
73
90
|
pi.sendMessage(
|
|
74
91
|
{
|
|
75
92
|
content: adviceForGateText(result),
|
|
76
93
|
customType: "advisor-loop-result",
|
|
77
|
-
details
|
|
78
|
-
advisor: result.model,
|
|
79
|
-
decision: result.decision,
|
|
80
|
-
text: result.markdown,
|
|
81
|
-
...(snapshotAdvisorUsage(result.usage)
|
|
82
|
-
? { usage: snapshotAdvisorUsage(result.usage) }
|
|
83
|
-
: {}),
|
|
84
|
-
},
|
|
94
|
+
details,
|
|
85
95
|
display: true,
|
|
86
96
|
},
|
|
87
97
|
{ deliverAs: "steer" }
|
|
@@ -118,7 +128,7 @@ const applyGateDecision = (
|
|
|
118
128
|
sendAutomaticGateFailure(
|
|
119
129
|
pi,
|
|
120
130
|
`**Advisor gate failure (${result.category}):** ${result.message}`,
|
|
121
|
-
result.usage
|
|
131
|
+
snapshotAdvisorUsage(result.usage)
|
|
122
132
|
);
|
|
123
133
|
return failure.block
|
|
124
134
|
? { block: true, reason: `${reason}\n${failure.reason}` }
|
|
@@ -158,6 +168,7 @@ export const handleAutomaticGate = async (
|
|
|
158
168
|
if (
|
|
159
169
|
isSimpleMode() ||
|
|
160
170
|
event.toolName === "ask_advisor" ||
|
|
171
|
+
!advisorModelIsAllowed(ctx) ||
|
|
161
172
|
!advisorAutoLoopGateRef ||
|
|
162
173
|
!session.recordToolCall(
|
|
163
174
|
event.toolName,
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
|
|
3
|
+
import { advisorModelWhitelistRef } from "../config/state.ts";
|
|
4
|
+
|
|
5
|
+
type AdvisorModelAccess =
|
|
6
|
+
| { allowed: true; modelRef?: string }
|
|
7
|
+
| { allowed: false; modelRef?: string; reason: string };
|
|
8
|
+
|
|
9
|
+
const currentModelRef = (
|
|
10
|
+
ctx: Pick<ExtensionContext, "model">
|
|
11
|
+
): string | undefined => {
|
|
12
|
+
const { model } = ctx;
|
|
13
|
+
return model ? `${model.provider}/${model.id}` : undefined;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const advisorModelAccess = (
|
|
17
|
+
ctx: Pick<ExtensionContext, "model">
|
|
18
|
+
): AdvisorModelAccess => {
|
|
19
|
+
const modelRef = currentModelRef(ctx);
|
|
20
|
+
if (advisorModelWhitelistRef.length === 0) {
|
|
21
|
+
return modelRef ? { allowed: true, modelRef } : { allowed: true };
|
|
22
|
+
}
|
|
23
|
+
if (modelRef && advisorModelWhitelistRef.includes(modelRef)) {
|
|
24
|
+
return { allowed: true, modelRef };
|
|
25
|
+
}
|
|
26
|
+
const current = modelRef ?? "no current model";
|
|
27
|
+
const denial: AdvisorModelAccess = {
|
|
28
|
+
allowed: false,
|
|
29
|
+
reason: `Advisor calls are restricted to the configured model whitelist (${advisorModelWhitelistRef.join(", ")}). Current model: ${current}.`,
|
|
30
|
+
};
|
|
31
|
+
if (modelRef) {
|
|
32
|
+
denial.modelRef = modelRef;
|
|
33
|
+
}
|
|
34
|
+
return denial;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export const advisorModelIsAllowed = (
|
|
38
|
+
ctx: Pick<ExtensionContext, "model">
|
|
39
|
+
): boolean => advisorModelAccess(ctx).allowed;
|
|
40
|
+
|
|
41
|
+
export const advisorModelAccessReason = (
|
|
42
|
+
ctx: Pick<ExtensionContext, "model">
|
|
43
|
+
): string | undefined => {
|
|
44
|
+
const access = advisorModelAccess(ctx);
|
|
45
|
+
return access.allowed ? undefined : access.reason;
|
|
46
|
+
};
|