pi-advisor-flow 0.5.6 → 0.7.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 +29 -0
- package/README.md +1 -0
- package/dist/index.js +3160 -1255
- package/extensions/index.ts +9 -9
- package/package.json +12 -8
- package/src/{untracked.ts → attachments.ts} +2 -5
- package/src/commands/activation-preparation.ts +6 -6
- package/src/commands/activation.ts +7 -7
- package/src/commands/lifecycle.ts +5 -5
- package/src/commands/manual-command.ts +13 -9
- package/src/commands/manual-consultation.ts +24 -10
- package/src/commands/manual-progress.ts +3 -3
- package/src/commands/model-commands.ts +6 -6
- package/src/commands/model-options.ts +4 -4
- package/src/commands/model-picker.ts +2 -2
- package/src/commands/registration.ts +8 -8
- package/src/commands/renderers.ts +5 -5
- package/src/commands/runtime.ts +8 -8
- package/src/commands/settings-commands.ts +8 -8
- package/src/commands/settings-persistence.ts +52 -4
- package/src/commands/types.ts +6 -13
- package/src/commands.ts +2 -2
- package/src/config/args.ts +3 -3
- package/src/config/defaults.ts +78 -3
- package/src/config/schema.ts +434 -0
- package/src/config/state.ts +71 -2
- package/src/config/storage.ts +27 -119
- package/src/config/types.ts +30 -1
- package/src/config/validation.ts +61 -181
- package/src/config.ts +7 -7
- package/src/content-utils.ts +30 -0
- package/src/conversation.ts +13 -168
- package/src/git.ts +4 -18
- package/src/herdr.ts +6 -8
- package/src/jev/client.ts +348 -0
- package/src/jev/key-store.ts +252 -0
- package/src/jev/ledger.ts +234 -0
- package/src/jev/questions.ts +119 -0
- package/src/jev/state.ts +47 -0
- package/src/jev/transport.ts +58 -0
- package/src/model-stream.ts +5 -8
- package/src/outcomes.ts +3 -2
- package/src/pi-settings.ts +59 -0
- package/src/preferences.ts +2 -2
- package/src/redaction.ts +50 -0
- package/src/scout-context.ts +121 -485
- package/src/scout-curation.ts +84 -0
- package/src/scout-groups.ts +485 -0
- package/src/scout-protocol.ts +122 -0
- package/src/scout-reconstruct.ts +44 -0
- package/src/scout-types.ts +66 -0
- package/src/scout.ts +53 -63
- package/src/session-state.ts +215 -78
- package/src/tool-result-cap.ts +88 -0
- package/src/tools/consult-context.ts +155 -0
- package/src/tools/consultation.ts +71 -220
- package/src/tools/gate-policy.ts +5 -5
- package/src/tools/gate-protocol.ts +2 -2
- package/src/tools/jev-filter.ts +202 -0
- package/src/tools/jev-turn-gate.ts +183 -0
- package/src/tools/loop-gate.ts +71 -61
- package/src/tools/outage-notifier.ts +30 -0
- package/src/tools/prompts.ts +4 -7
- package/src/tools/register-ask-advisor.ts +46 -15
- package/src/tools/register-lifecycle.ts +7 -7
- package/src/tools/register-outcome.ts +5 -5
- package/src/tools/register-renderers.ts +63 -5
- package/src/tools/registration.ts +27 -10
- package/src/tools/render-advisor-result.ts +54 -22
- package/src/tools/render-common.ts +25 -13
- package/src/tools/scout-status.ts +5 -5
- package/src/tools/session.ts +1 -1
- package/src/tools/types.ts +17 -7
- package/src/tools.ts +9 -9
- package/src/ui/jev-setup-submenu.ts +344 -0
- package/src/ui/manual-dialog-render.ts +2 -2
- package/src/ui/manual-dialog.ts +4 -9
- package/src/ui/masked-input.ts +68 -0
- package/src/ui/model-selector.ts +1 -1
- package/src/ui/settings-formatting.ts +2 -2
- package/src/ui/settings-items.ts +166 -5
- package/src/ui/settings-mutations.ts +37 -2
- package/src/ui/settings-selector.ts +5 -5
- package/src/ui/text-setting-submenu.ts +1 -1
- package/src/ui/types.ts +13 -2
- package/src/ui.ts +4 -4
- package/src/usage.ts +4 -7
|
@@ -3,201 +3,60 @@ import type { Message } from "@earendil-works/pi-ai/compat";
|
|
|
3
3
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
4
4
|
import {
|
|
5
5
|
advisorEffortRef,
|
|
6
|
-
advisorGitContextMaxCharsRef,
|
|
7
|
-
advisorGitContextRef,
|
|
8
6
|
advisorRedactSecretsRef,
|
|
9
7
|
advisorRef,
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
} from "../
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
clampGitContextLevel,
|
|
20
|
-
collectGitContext,
|
|
21
|
-
type GitContextLevel,
|
|
22
|
-
} from "../git.js";
|
|
23
|
-
import { collectTextStream, resolveConfiguredModel } from "../model-stream.js";
|
|
24
|
-
import { readProjectPreferences } from "../preferences.js";
|
|
25
|
-
import {
|
|
26
|
-
runAdvisorScout,
|
|
27
|
-
type ScoutLifecycleEvent,
|
|
28
|
-
type ScoutOutcome,
|
|
29
|
-
} from "../scout.js";
|
|
30
|
-
import {
|
|
31
|
-
buildScoutManifest,
|
|
32
|
-
reconstructScoutConversation,
|
|
33
|
-
SCOUT_MANIFEST_MAX_BYTES,
|
|
34
|
-
} from "../scout-context.js";
|
|
35
|
-
import type { ConsultationTrigger, GateTrigger } from "../session-state.js";
|
|
36
|
-
import { readTrackedFiles, readUntrackedFiles } from "../untracked.js";
|
|
37
|
-
import { parseAutomaticDecision } from "./gate-protocol.js";
|
|
8
|
+
} from "../config/state.ts";
|
|
9
|
+
import { loadConfig } from "../config/storage.ts";
|
|
10
|
+
import type { GitContextLevel } from "../git.ts";
|
|
11
|
+
import { collectTextStream, resolveConfiguredModel } from "../model-stream.ts";
|
|
12
|
+
import { redactSecrets } from "../redaction.ts";
|
|
13
|
+
import type { ScoutLifecycleEvent } from "../scout.ts";
|
|
14
|
+
import type { ConsultationTrigger, GateTrigger } from "../session-state.ts";
|
|
15
|
+
import { assembleConsultationContext } from "./consult-context.ts";
|
|
16
|
+
import { parseAutomaticDecision } from "./gate-protocol.ts";
|
|
38
17
|
import {
|
|
39
18
|
ADVISOR_DECISION_SYSTEM,
|
|
40
19
|
ADVISOR_SYSTEM,
|
|
41
|
-
advisorGitContextBudget,
|
|
42
20
|
advisorMessageText,
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
} from "./prompts.js";
|
|
46
|
-
import type { AdvisorConsultationResult, AdvisorGateOutcome } from "./types.js";
|
|
21
|
+
} from "./prompts.ts";
|
|
22
|
+
import type { AdvisorConsultationResult, AdvisorGateOutcome } from "./types.ts";
|
|
47
23
|
|
|
48
|
-
export
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
maxChars?: number
|
|
57
|
-
): Promise<{
|
|
58
|
-
conversation: string;
|
|
59
|
-
scout?: Exclude<ScoutOutcome, { cancelled: true }>;
|
|
60
|
-
}> => {
|
|
61
|
-
if (!enabled) {
|
|
62
|
-
return { conversation: legacyConversation };
|
|
63
|
-
}
|
|
64
|
-
if (maxChars !== undefined && maxChars <= 0) {
|
|
65
|
-
return { conversation: "" };
|
|
66
|
-
}
|
|
67
|
-
const built = buildScoutManifest(ctx, {
|
|
68
|
-
currentInvocationId,
|
|
69
|
-
maxConversationChars: maxChars,
|
|
70
|
-
maxManifestBytes: SCOUT_MANIFEST_MAX_BYTES,
|
|
71
|
-
});
|
|
72
|
-
if (!built.ok) {
|
|
73
|
-
const scout: Exclude<ScoutOutcome, { cancelled: true }> = {
|
|
74
|
-
category: built.reason,
|
|
75
|
-
message: built.message,
|
|
76
|
-
metrics: {
|
|
77
|
-
availableCount: 0,
|
|
78
|
-
inputBytes: 0,
|
|
79
|
-
latencyMs: 0,
|
|
80
|
-
omittedBeforeScout: 0,
|
|
81
|
-
selectedCount: 0,
|
|
82
|
-
},
|
|
83
|
-
model: executorRef,
|
|
84
|
-
ok: false,
|
|
85
|
-
};
|
|
86
|
-
onScout?.({ outcome: scout, type: "fallback" });
|
|
87
|
-
return { conversation: legacyConversation, scout };
|
|
88
|
-
}
|
|
89
|
-
const outcome = await runScout(
|
|
90
|
-
ctx,
|
|
91
|
-
built.manifest,
|
|
92
|
-
signal,
|
|
93
|
-
onScout,
|
|
94
|
-
undefined,
|
|
95
|
-
undefined
|
|
96
|
-
);
|
|
97
|
-
if (!outcome.ok && outcome.cancelled) {
|
|
98
|
-
throw signal?.reason instanceof Error
|
|
99
|
-
? signal.reason
|
|
100
|
-
: new Error("Advisor operation cancelled during Scout.");
|
|
101
|
-
}
|
|
102
|
-
let conversation = legacyConversation;
|
|
103
|
-
if (outcome.ok) {
|
|
104
|
-
conversation =
|
|
105
|
-
maxChars === undefined
|
|
106
|
-
? outcome.conversation
|
|
107
|
-
: reconstructScoutConversation(
|
|
108
|
-
built.manifest,
|
|
109
|
-
outcome.selection.selectedIds,
|
|
110
|
-
outcome.selection.synthesis,
|
|
111
|
-
maxChars
|
|
112
|
-
);
|
|
24
|
+
// biome-ignore lint/performance/noBarrelFile: preserves the tools facade's historical re-export of the moved curation entry point.
|
|
25
|
+
export { curateAdvisorConversation } from "../scout-curation.ts";
|
|
26
|
+
|
|
27
|
+
/** Thrown when the Advisor produced an empty response body. */
|
|
28
|
+
class AdvisorNoAdviceError extends Error {
|
|
29
|
+
constructor() {
|
|
30
|
+
super("Advisor returned no advice.");
|
|
31
|
+
this.name = "AdvisorNoAdviceError";
|
|
113
32
|
}
|
|
114
|
-
|
|
115
|
-
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface CollectAdvisorResponseOptions {
|
|
36
|
+
ctx: ExtensionContext;
|
|
37
|
+
currentInvocationId?: string;
|
|
38
|
+
draft?: string;
|
|
39
|
+
gitContext?: GitContextLevel;
|
|
40
|
+
includeTracked?: string[];
|
|
41
|
+
includeUntracked?: string[];
|
|
42
|
+
onChunk?: (thinking: string, text: string) => void;
|
|
43
|
+
onScout?: (event: ScoutLifecycleEvent) => void;
|
|
44
|
+
question?: string;
|
|
45
|
+
signal?: AbortSignal;
|
|
46
|
+
systemPrompt: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const fileTag = (item: { path: string; text: string }) =>
|
|
50
|
+
`<file path=${JSON.stringify(item.path)}>\n${item.text}\n</file>`;
|
|
116
51
|
|
|
117
52
|
const collectAdvisorResponse = async (
|
|
118
|
-
|
|
119
|
-
systemPrompt: string,
|
|
120
|
-
question: string | undefined,
|
|
121
|
-
signal: AbortSignal | undefined,
|
|
122
|
-
onChunk: ((thinking: string, text: string) => void) | undefined,
|
|
123
|
-
gitContext?: GitContextLevel,
|
|
124
|
-
draft?: string,
|
|
125
|
-
includeUntracked?: string[],
|
|
126
|
-
includeTracked?: string[],
|
|
127
|
-
onScout?: (event: ScoutLifecycleEvent) => void,
|
|
128
|
-
currentInvocationId?: string
|
|
53
|
+
options: CollectAdvisorResponseOptions
|
|
129
54
|
) => {
|
|
55
|
+
const { ctx, question, signal, systemPrompt } = options;
|
|
130
56
|
loadConfig(ctx);
|
|
131
57
|
const resolved = await resolveConfiguredModel(ctx, advisorRef, "Advisor");
|
|
58
|
+
const context = await assembleConsultationContext(options);
|
|
132
59
|
|
|
133
|
-
// The user setting is the ceiling; the Executor may only narrow it.
|
|
134
|
-
const allowed = advisorGitContextRef;
|
|
135
|
-
const level = clampGitContextLevel(gitContext ?? allowed, allowed);
|
|
136
|
-
const gitBudget = advisorGitContextBudget(
|
|
137
|
-
contextMaxCharsRef,
|
|
138
|
-
advisorGitContextMaxCharsRef
|
|
139
|
-
);
|
|
140
|
-
const changes = collectGitContext(
|
|
141
|
-
ctx.cwd,
|
|
142
|
-
level,
|
|
143
|
-
gitBudget,
|
|
144
|
-
advisorRedactSecretsRef ? redactSecrets : undefined
|
|
145
|
-
);
|
|
146
|
-
// The note is placed first so a cap can never drop the statement that the
|
|
147
|
-
// Advisor's view of the repository is limited.
|
|
148
|
-
// The disclosure warning is control metadata, not repository payload. Keep it
|
|
149
|
-
// outside the zero-byte Git budget so disabling disclosure cannot erase it.
|
|
150
|
-
const changeText = advisorRepositoryContext(
|
|
151
|
-
changes,
|
|
152
|
-
gitContext ?? allowed,
|
|
153
|
-
level,
|
|
154
|
-
gitBudget
|
|
155
|
-
);
|
|
156
|
-
// Repository context spends part of the shared budget, so a large patch
|
|
157
|
-
// cannot silently push the conversation past the model's context window.
|
|
158
|
-
const conversationBudget = Math.max(
|
|
159
|
-
0,
|
|
160
|
-
contextMaxCharsRef - changeText.length
|
|
161
|
-
);
|
|
162
|
-
const legacyConversation = advisorRequestConversation(
|
|
163
|
-
ctx,
|
|
164
|
-
conversationBudget
|
|
165
|
-
);
|
|
166
|
-
const curated = await curateAdvisorConversation(
|
|
167
|
-
ctx,
|
|
168
|
-
legacyConversation,
|
|
169
|
-
signal,
|
|
170
|
-
onScout,
|
|
171
|
-
advisorScoutEnabledRef,
|
|
172
|
-
runAdvisorScout,
|
|
173
|
-
currentInvocationId,
|
|
174
|
-
conversationBudget
|
|
175
|
-
);
|
|
176
|
-
const { conversation, scout } = curated;
|
|
177
|
-
const preferences = await readProjectPreferences(
|
|
178
|
-
ctx,
|
|
179
|
-
8 * 1024,
|
|
180
|
-
advisorRedactSecretsRef
|
|
181
|
-
);
|
|
182
|
-
const draftText = draft
|
|
183
|
-
? redactAndCapText(draft, 8 * 1024, advisorRedactSecretsRef)
|
|
184
|
-
: undefined;
|
|
185
|
-
const untracked = await readUntrackedFiles(
|
|
186
|
-
ctx.cwd,
|
|
187
|
-
includeUntracked ?? [],
|
|
188
|
-
advisorUntrackedContentRef,
|
|
189
|
-
advisorRedactSecretsRef
|
|
190
|
-
);
|
|
191
|
-
const tracked = await readTrackedFiles(
|
|
192
|
-
ctx.cwd,
|
|
193
|
-
includeTracked ?? [],
|
|
194
|
-
advisorTrackedFileContentRef,
|
|
195
|
-
advisorRedactSecretsRef,
|
|
196
|
-
Math.max(
|
|
197
|
-
0,
|
|
198
|
-
24 * 1024 - untracked.reduce((sum, item) => sum + item.bytes, 0)
|
|
199
|
-
)
|
|
200
|
-
);
|
|
201
60
|
// Keep the original question available to local UI callers, but never send
|
|
202
61
|
// its credential-shaped values to the provider when redaction is enabled.
|
|
203
62
|
const outboundQuestion =
|
|
@@ -209,19 +68,13 @@ const collectAdvisorResponse = async (
|
|
|
209
68
|
content: [
|
|
210
69
|
{
|
|
211
70
|
text: advisorMessageText(
|
|
212
|
-
conversation,
|
|
71
|
+
context.conversation,
|
|
213
72
|
outboundQuestion,
|
|
214
|
-
changeText,
|
|
215
|
-
draftText,
|
|
216
|
-
preferences?.text,
|
|
217
|
-
untracked.map(
|
|
218
|
-
|
|
219
|
-
`<file path=${JSON.stringify(item.path)}>\n${item.text}\n</file>`
|
|
220
|
-
),
|
|
221
|
-
tracked.map(
|
|
222
|
-
(item) =>
|
|
223
|
-
`<file path=${JSON.stringify(item.path)}>\n${item.text}\n</file>`
|
|
224
|
-
)
|
|
73
|
+
context.changeText,
|
|
74
|
+
context.draftText,
|
|
75
|
+
context.preferences?.text,
|
|
76
|
+
context.untracked.map(fileTag),
|
|
77
|
+
context.tracked.map(fileTag)
|
|
225
78
|
),
|
|
226
79
|
type: "text",
|
|
227
80
|
},
|
|
@@ -233,27 +86,29 @@ const collectAdvisorResponse = async (
|
|
|
233
86
|
|
|
234
87
|
const streamed = await collectTextStream(resolved, {
|
|
235
88
|
messages,
|
|
236
|
-
onChunk,
|
|
89
|
+
onChunk: options.onChunk,
|
|
237
90
|
reasoning: advisorEffortRef,
|
|
238
91
|
signal,
|
|
239
92
|
systemPrompt,
|
|
240
93
|
});
|
|
241
94
|
const markdown = streamed.text;
|
|
242
95
|
if (!markdown.trim()) {
|
|
243
|
-
throw new
|
|
96
|
+
throw new AdvisorNoAdviceError();
|
|
244
97
|
}
|
|
245
98
|
return {
|
|
246
|
-
draftBytes:
|
|
99
|
+
draftBytes: context.draftText
|
|
100
|
+
? Buffer.byteLength(context.draftText, "utf8")
|
|
101
|
+
: undefined,
|
|
247
102
|
markdown,
|
|
248
103
|
model: advisorRef,
|
|
249
|
-
preferenceBytes: preferences?.bytes,
|
|
104
|
+
preferenceBytes: context.preferences?.bytes,
|
|
250
105
|
thinkingText: streamed.thinking,
|
|
251
106
|
trackedBytes:
|
|
252
|
-
tracked.reduce((sum, item) => sum + item.bytes, 0) || undefined,
|
|
107
|
+
context.tracked.reduce((sum, item) => sum + item.bytes, 0) || undefined,
|
|
253
108
|
untrackedBytes:
|
|
254
|
-
untracked.reduce((sum, item) => sum + item.bytes, 0) || undefined,
|
|
109
|
+
context.untracked.reduce((sum, item) => sum + item.bytes, 0) || undefined,
|
|
255
110
|
usage: streamed.usage,
|
|
256
|
-
...(scout ? { scout } : {}),
|
|
111
|
+
...(context.scout ? { scout: context.scout } : {}),
|
|
257
112
|
};
|
|
258
113
|
};
|
|
259
114
|
|
|
@@ -270,19 +125,19 @@ export const consultAdvisor = async (
|
|
|
270
125
|
onScout?: (event: ScoutLifecycleEvent) => void,
|
|
271
126
|
currentInvocationId?: string
|
|
272
127
|
): Promise<AdvisorConsultationResult> => {
|
|
273
|
-
const result = await collectAdvisorResponse(
|
|
128
|
+
const result = await collectAdvisorResponse({
|
|
274
129
|
ctx,
|
|
275
|
-
|
|
276
|
-
question,
|
|
277
|
-
signal,
|
|
278
|
-
onChunk,
|
|
279
|
-
gitContext,
|
|
130
|
+
currentInvocationId,
|
|
280
131
|
draft,
|
|
281
|
-
|
|
132
|
+
gitContext,
|
|
282
133
|
includeTracked,
|
|
134
|
+
includeUntracked,
|
|
135
|
+
onChunk,
|
|
283
136
|
onScout,
|
|
284
|
-
|
|
285
|
-
|
|
137
|
+
question,
|
|
138
|
+
signal,
|
|
139
|
+
systemPrompt: ADVISOR_SYSTEM,
|
|
140
|
+
});
|
|
286
141
|
return { ...result, adviceId: randomUUID(), trigger };
|
|
287
142
|
};
|
|
288
143
|
|
|
@@ -296,19 +151,15 @@ export const runAdvisorGate = async (
|
|
|
296
151
|
currentInvocationId?: string
|
|
297
152
|
): Promise<AdvisorGateOutcome> => {
|
|
298
153
|
try {
|
|
299
|
-
const result = await collectAdvisorResponse(
|
|
154
|
+
const result = await collectAdvisorResponse({
|
|
300
155
|
ctx,
|
|
301
|
-
|
|
302
|
-
question,
|
|
303
|
-
signal,
|
|
156
|
+
currentInvocationId,
|
|
304
157
|
onChunk,
|
|
305
|
-
undefined,
|
|
306
|
-
undefined,
|
|
307
|
-
undefined,
|
|
308
|
-
undefined,
|
|
309
158
|
onScout,
|
|
310
|
-
|
|
311
|
-
|
|
159
|
+
question,
|
|
160
|
+
signal,
|
|
161
|
+
systemPrompt: ADVISOR_DECISION_SYSTEM,
|
|
162
|
+
});
|
|
312
163
|
const parsed = parseAutomaticDecision(result.markdown);
|
|
313
164
|
if (!parsed.ok) {
|
|
314
165
|
return { ...parsed, usage: result.usage };
|
|
@@ -327,7 +178,7 @@ export const runAdvisorGate = async (
|
|
|
327
178
|
const message = error instanceof Error ? error.message : String(error);
|
|
328
179
|
return {
|
|
329
180
|
category:
|
|
330
|
-
|
|
181
|
+
error instanceof AdvisorNoAdviceError
|
|
331
182
|
? "empty-response"
|
|
332
183
|
: "provider-error",
|
|
333
184
|
message,
|
package/src/tools/gate-policy.ts
CHANGED
|
@@ -8,11 +8,11 @@ import {
|
|
|
8
8
|
getAdvisorMaxCallsPerSession,
|
|
9
9
|
getAdvisorSettings,
|
|
10
10
|
isSimpleMode,
|
|
11
|
-
} from "../config/state.
|
|
12
|
-
import type { GateFailureMode } from "../config/types.
|
|
13
|
-
import { herdrAdvisorBlock, notifyHerdrAdvisorFailure } from "../herdr.
|
|
14
|
-
import type { AdvisorSessionState } from "../session-state.
|
|
15
|
-
import type { AdvisorGateResult, GateFailureCategory } from "./types.
|
|
11
|
+
} from "../config/state.ts";
|
|
12
|
+
import type { GateFailureMode } from "../config/types.ts";
|
|
13
|
+
import { herdrAdvisorBlock, notifyHerdrAdvisorFailure } from "../herdr.ts";
|
|
14
|
+
import type { AdvisorSessionState } from "../session-state.ts";
|
|
15
|
+
import type { AdvisorGateResult, GateFailureCategory } from "./types.ts";
|
|
16
16
|
|
|
17
17
|
export const updateAdvisorUsageStatus = (
|
|
18
18
|
ctx: ExtensionContext,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { GateDecision } from "../session-state.
|
|
2
|
-
import type { AdvisorGateFailure, AdvisorGateResult } from "./types.
|
|
1
|
+
import type { GateDecision } from "../session-state.ts";
|
|
2
|
+
import type { AdvisorGateFailure, AdvisorGateResult } from "./types.ts";
|
|
3
3
|
|
|
4
4
|
const DECISION_LINE = /^Decision\s*:\s*(proceed|revise|blocked)\s*$/i;
|
|
5
5
|
const CODE_FENCE = /^ {0,3}(`{3,}|~{3,})(.*)$/;
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { Fetch } from "@typesafe-ai/sdk";
|
|
3
|
+
import {
|
|
4
|
+
advisorJevFilterEnabledRef,
|
|
5
|
+
advisorJevFilterNoulMarginRef,
|
|
6
|
+
advisorJevFilterOverrideWindowRef,
|
|
7
|
+
advisorJevFilterSkipConfidenceRef,
|
|
8
|
+
isSimpleMode,
|
|
9
|
+
} from "../config/state.ts";
|
|
10
|
+
import { JevFailure, jevClientFromCredentials } from "../jev/client.ts";
|
|
11
|
+
import { consumePlaintextKeyWarning } from "../jev/key-store.ts";
|
|
12
|
+
import {
|
|
13
|
+
composeScreeningVerdict,
|
|
14
|
+
screeningQuestions,
|
|
15
|
+
} from "../jev/questions.ts";
|
|
16
|
+
import { buildJevState } from "../jev/state.ts";
|
|
17
|
+
import { type JevCredentials, resolveJevTransport } from "../jev/transport.ts";
|
|
18
|
+
import type { AdvisorSessionState } from "../session-state.ts";
|
|
19
|
+
import { createOutageNotifier } from "./outage-notifier.ts";
|
|
20
|
+
|
|
21
|
+
export type JevSkipKind = "screened" | "repeat";
|
|
22
|
+
|
|
23
|
+
export type ScreeningOutcome =
|
|
24
|
+
| { decision: "allow" }
|
|
25
|
+
| {
|
|
26
|
+
decision: "skip";
|
|
27
|
+
kind: JevSkipKind;
|
|
28
|
+
reason: string;
|
|
29
|
+
reattachedAdvice?: string;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export interface ScreenConsultationOptions {
|
|
33
|
+
draft?: string;
|
|
34
|
+
force?: boolean;
|
|
35
|
+
question?: string;
|
|
36
|
+
signal?: AbortSignal;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface ScreeningDeps {
|
|
40
|
+
fetch?: Fetch;
|
|
41
|
+
resolveTransport?: () => Promise<JevCredentials | undefined>;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export const normalizeScreeningQuestion = (question: string | undefined) =>
|
|
45
|
+
question?.trim().toLowerCase().replace(/\s+/g, " ") || undefined;
|
|
46
|
+
|
|
47
|
+
const REATTACHED_ADVICE_CAP_BYTES = 4 * 1024;
|
|
48
|
+
|
|
49
|
+
const SCREENED_SKIP_TEXT =
|
|
50
|
+
"Advisor consultation skipped (screened out): the stakes are low and you can resolve this yourself with available tools and context. Proceed on your own judgment with what you already have.";
|
|
51
|
+
|
|
52
|
+
const repeatSkipText = (advice: string) =>
|
|
53
|
+
`Advisor consultation skipped (already answered): this question was answered earlier in this session; the earlier advice is reattached below. Consult again only if the situation has materially changed.\n\n${advice}`;
|
|
54
|
+
|
|
55
|
+
const outageNotifier = createOutageNotifier(
|
|
56
|
+
(category, message) =>
|
|
57
|
+
`Advisor Jev filter failed (${category}); allowing consultations. ${message}`
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
const notifyOutageOnce = outageNotifier.notify;
|
|
61
|
+
|
|
62
|
+
/** Test-only: re-arms the once-per-outage notification. */
|
|
63
|
+
export const resetJevOutageNotification = outageNotifier.reset;
|
|
64
|
+
|
|
65
|
+
const allow = (): ScreeningOutcome => ({ decision: "allow" });
|
|
66
|
+
|
|
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
|
+
const bypassOutcome = (
|
|
107
|
+
session: AdvisorSessionState,
|
|
108
|
+
options: ScreenConsultationOptions,
|
|
109
|
+
normalizedQuestion: string | undefined
|
|
110
|
+
): ScreeningOutcome | undefined => {
|
|
111
|
+
const lastSkip = session.lastJevSkip;
|
|
112
|
+
if (options.force) {
|
|
113
|
+
if (
|
|
114
|
+
lastSkip?.normalizedQuestion !== undefined &&
|
|
115
|
+
lastSkip.normalizedQuestion === normalizedQuestion
|
|
116
|
+
) {
|
|
117
|
+
session.recordJevFilterOverride();
|
|
118
|
+
}
|
|
119
|
+
return allow();
|
|
120
|
+
}
|
|
121
|
+
if (
|
|
122
|
+
normalizedQuestion !== undefined &&
|
|
123
|
+
lastSkip?.normalizedQuestion === normalizedQuestion &&
|
|
124
|
+
session.sessionTurnOrdinal - lastSkip.turn <=
|
|
125
|
+
advisorJevFilterOverrideWindowRef
|
|
126
|
+
) {
|
|
127
|
+
session.recordJevFilterOverride();
|
|
128
|
+
return allow();
|
|
129
|
+
}
|
|
130
|
+
return undefined;
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const screenWithJev = async (
|
|
134
|
+
ctx: ExtensionContext,
|
|
135
|
+
session: AdvisorSessionState,
|
|
136
|
+
options: ScreenConsultationOptions,
|
|
137
|
+
deps: ScreeningDeps,
|
|
138
|
+
normalizedQuestion: string | undefined
|
|
139
|
+
): Promise<ScreeningOutcome> => {
|
|
140
|
+
const credentials = await (deps.resolveTransport ?? resolveJevTransport)(ctx);
|
|
141
|
+
if (!credentials) {
|
|
142
|
+
session.recordJevFilterFailure();
|
|
143
|
+
notifyOutageOnce(
|
|
144
|
+
ctx,
|
|
145
|
+
"missing-key",
|
|
146
|
+
"No Jev credentials resolved (no TypeSafe key and no OpenRouter login)."
|
|
147
|
+
);
|
|
148
|
+
return allow();
|
|
149
|
+
}
|
|
150
|
+
if (credentials.source === "advisor-json") {
|
|
151
|
+
const warning = consumePlaintextKeyWarning();
|
|
152
|
+
if (warning && ctx.hasUI) {
|
|
153
|
+
ctx.ui.notify(warning, "warning");
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const client = jevClientFromCredentials(credentials, deps.fetch);
|
|
158
|
+
try {
|
|
159
|
+
const result = await client.ask(
|
|
160
|
+
buildJevState(ctx, options),
|
|
161
|
+
screeningQuestions,
|
|
162
|
+
options.signal
|
|
163
|
+
);
|
|
164
|
+
session.recordJevFilterUsage(result.usage);
|
|
165
|
+
const verdict = composeScreeningVerdict(result.answers, {
|
|
166
|
+
noulMargin: advisorJevFilterNoulMarginRef,
|
|
167
|
+
skipConfidence: advisorJevFilterSkipConfidenceRef,
|
|
168
|
+
});
|
|
169
|
+
if (verdict.skip) {
|
|
170
|
+
session.recordJevFilterSkipped(false, normalizedQuestion);
|
|
171
|
+
return {
|
|
172
|
+
decision: "skip",
|
|
173
|
+
kind: "screened",
|
|
174
|
+
reason: "low stakes and resolvable without a consultation",
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
session.recordJevFilterAllowed();
|
|
178
|
+
return allow();
|
|
179
|
+
} catch (error) {
|
|
180
|
+
session.recordJevFilterFailure();
|
|
181
|
+
if (error instanceof JevFailure) {
|
|
182
|
+
notifyOutageOnce(ctx, error.category, error.message);
|
|
183
|
+
} else if (options.signal?.aborted) {
|
|
184
|
+
throw error;
|
|
185
|
+
} else {
|
|
186
|
+
notifyOutageOnce(
|
|
187
|
+
ctx,
|
|
188
|
+
"error",
|
|
189
|
+
error instanceof Error ? error.message : String(error)
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
return allow();
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
export const screeningSkipText = (outcome: {
|
|
197
|
+
kind: JevSkipKind;
|
|
198
|
+
reattachedAdvice?: string;
|
|
199
|
+
}) =>
|
|
200
|
+
outcome.kind === "repeat" && outcome.reattachedAdvice
|
|
201
|
+
? repeatSkipText(outcome.reattachedAdvice)
|
|
202
|
+
: SCREENED_SKIP_TEXT;
|