pi-advisor-flow 0.7.0 → 0.8.1-dev.1
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 +27 -18
- package/README.md +9 -8
- package/dist/index.js +1968 -1081
- package/extensions/index.ts +2 -1
- package/package.json +48 -35
- package/src/attachments.ts +11 -10
- package/src/child-session.ts +15 -0
- package/src/commands/activation-preparation.ts +1 -0
- package/src/commands/activation.ts +12 -3
- package/src/commands/lifecycle.ts +7 -1
- 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 +128 -77
- 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/session-state.ts
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { isRecordOf, isString } from "./content-utils.ts";
|
|
2
|
+
import type { JsonValue } from "./content-utils.ts";
|
|
3
|
+
import { AdvisorJevLedgerState } from "./jev/ledger.ts";
|
|
4
|
+
import type { AdvisorJevUsage } from "./jev/ledger.ts";
|
|
2
5
|
import {
|
|
3
|
-
type AdvisorUsageTotals,
|
|
4
6
|
addAdvisorUsage,
|
|
5
7
|
emptyAdvisorUsageTotals,
|
|
6
8
|
formatAdvisorUsageStatus,
|
|
7
9
|
formatAdvisorUsageTotals,
|
|
8
10
|
} from "./usage.ts";
|
|
11
|
+
import type { AdvisorUsageTotals } from "./usage.ts";
|
|
9
12
|
|
|
10
13
|
export type GateDecision = "proceed" | "revise" | "blocked";
|
|
11
14
|
export type ConsultationTrigger = "manual" | "executor-requested" | "turn-gate";
|
|
@@ -27,7 +30,7 @@ export interface AdvisorInvocationRecord {
|
|
|
27
30
|
usage?: unknown;
|
|
28
31
|
}
|
|
29
32
|
|
|
30
|
-
const WHITESPACE = /\s
|
|
33
|
+
const WHITESPACE = /\s/u;
|
|
31
34
|
const TIMESTAMP_KEYS = new Set([
|
|
32
35
|
"createdat",
|
|
33
36
|
"date",
|
|
@@ -37,7 +40,8 @@ const TIMESTAMP_KEYS = new Set([
|
|
|
37
40
|
"updatedat",
|
|
38
41
|
]);
|
|
39
42
|
const REQUEST_ID_KEYS = new Set(["correlationid", "requestid", "traceid"]);
|
|
40
|
-
const normalizedKey = (key: string) =>
|
|
43
|
+
const normalizedKey = (key: string) =>
|
|
44
|
+
key.replaceAll(/[-_]/gu, "").toLowerCase();
|
|
41
45
|
const isVolatileKey = (key: string, keys: Set<string>) =>
|
|
42
46
|
keys.has(normalizedKey(key));
|
|
43
47
|
|
|
@@ -75,15 +79,24 @@ const normalizeShellWhitespace = (command: string) => {
|
|
|
75
79
|
|
|
76
80
|
const normalizeString = (value: string) =>
|
|
77
81
|
value
|
|
78
|
-
.
|
|
79
|
-
.
|
|
82
|
+
.replaceAll(/\/(?:private\/)?tmp\/[^\s/]+/gu, "/tmp/<temporary>")
|
|
83
|
+
.replaceAll(/\/var\/folders\/[^\s/]+/gu, "/var/folders/<temporary>");
|
|
80
84
|
|
|
81
|
-
export
|
|
85
|
+
export type NormalizedToolInput =
|
|
86
|
+
| JsonValue
|
|
87
|
+
| undefined
|
|
88
|
+
| NormalizedToolInput[]
|
|
89
|
+
| { [key: string]: NormalizedToolInput };
|
|
90
|
+
|
|
91
|
+
export const normalizeToolInput = <Input>(
|
|
82
92
|
toolName: string,
|
|
83
|
-
input:
|
|
84
|
-
):
|
|
85
|
-
const visit = (
|
|
86
|
-
|
|
93
|
+
input: Input
|
|
94
|
+
): NormalizedToolInput => {
|
|
95
|
+
const visit = (
|
|
96
|
+
value: NormalizedToolInput,
|
|
97
|
+
key?: string
|
|
98
|
+
): NormalizedToolInput => {
|
|
99
|
+
if (isString(value)) {
|
|
87
100
|
if (key && isVolatileKey(key, TIMESTAMP_KEYS)) {
|
|
88
101
|
return "<timestamp>";
|
|
89
102
|
}
|
|
@@ -98,21 +111,23 @@ export const normalizeToolInput = (
|
|
|
98
111
|
if (Array.isArray(value)) {
|
|
99
112
|
return value.map((item) => visit(item));
|
|
100
113
|
}
|
|
101
|
-
if (value
|
|
102
|
-
const record = value as Record<string, unknown>;
|
|
114
|
+
if (isRecordOf(value)) {
|
|
103
115
|
return Object.fromEntries(
|
|
104
|
-
Object.keys(
|
|
105
|
-
.
|
|
106
|
-
.map((childKey) => [childKey, visit(
|
|
116
|
+
Object.keys(value)
|
|
117
|
+
.toSorted()
|
|
118
|
+
.map((childKey) => [childKey, visit(value[childKey], childKey)])
|
|
107
119
|
);
|
|
108
120
|
}
|
|
109
121
|
return value;
|
|
110
122
|
};
|
|
111
|
-
|
|
123
|
+
// SAFETY: Pi tool inputs are JSON-decoded tool arguments, so the root value is already NormalizedToolInput-shaped.
|
|
124
|
+
return visit(input as Input & NormalizedToolInput);
|
|
112
125
|
};
|
|
113
126
|
|
|
114
|
-
export const normalizedToolSignature = (
|
|
115
|
-
|
|
127
|
+
export const normalizedToolSignature = <Input>(
|
|
128
|
+
toolName: string,
|
|
129
|
+
input: Input
|
|
130
|
+
) => `${toolName}:${JSON.stringify(normalizeToolInput(toolName, input))}`;
|
|
116
131
|
|
|
117
132
|
interface RepetitionState {
|
|
118
133
|
count: number;
|
|
@@ -120,16 +135,15 @@ interface RepetitionState {
|
|
|
120
135
|
previousSignature?: string;
|
|
121
136
|
}
|
|
122
137
|
|
|
138
|
+
interface IssuedAdvice {
|
|
139
|
+
advice: string;
|
|
140
|
+
normalizedQuestion?: string;
|
|
141
|
+
trigger: ConsultationTrigger;
|
|
142
|
+
}
|
|
143
|
+
|
|
123
144
|
interface AdviceLedger {
|
|
124
145
|
draftConsultations: number;
|
|
125
|
-
issued: Map<
|
|
126
|
-
string,
|
|
127
|
-
{
|
|
128
|
-
advice: string;
|
|
129
|
-
normalizedQuestion?: string;
|
|
130
|
-
trigger: ConsultationTrigger;
|
|
131
|
-
}
|
|
132
|
-
>;
|
|
146
|
+
issued: Map<string, IssuedAdvice>;
|
|
133
147
|
lastAdvice?: string;
|
|
134
148
|
outcomes: number;
|
|
135
149
|
pending: Set<string>;
|
|
@@ -196,7 +210,7 @@ export class AdvisorSessionState {
|
|
|
196
210
|
this.#blockedReason ??= reason;
|
|
197
211
|
}
|
|
198
212
|
|
|
199
|
-
recordToolCall(toolName: string, input:
|
|
213
|
+
recordToolCall<Input>(toolName: string, input: Input, threshold: number) {
|
|
200
214
|
if (toolName === "ask_advisor") {
|
|
201
215
|
return false;
|
|
202
216
|
}
|
|
@@ -269,11 +283,11 @@ export class AdvisorSessionState {
|
|
|
269
283
|
draft = false,
|
|
270
284
|
normalizedQuestion?: string
|
|
271
285
|
) {
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
286
|
+
const issued: IssuedAdvice = { advice, trigger };
|
|
287
|
+
if (normalizedQuestion) {
|
|
288
|
+
issued.normalizedQuestion = normalizedQuestion;
|
|
289
|
+
}
|
|
290
|
+
this.#ledger.issued.set(id, issued);
|
|
277
291
|
this.#ledger.lastAdvice = advice;
|
|
278
292
|
if (draft) {
|
|
279
293
|
this.#ledger.draftConsultations += 1;
|
|
@@ -300,12 +314,11 @@ export class AdvisorSessionState {
|
|
|
300
314
|
return false;
|
|
301
315
|
}
|
|
302
316
|
const mentioned = paths.every((path) => {
|
|
303
|
-
const escaped = path.
|
|
304
|
-
const boundary =
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
return new RegExp(boundary).test(advice);
|
|
317
|
+
const escaped = path.replaceAll(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
|
318
|
+
const boundary = `(^|[\\s"'\`()\\[])${
|
|
319
|
+
escaped
|
|
320
|
+
}(?=$|[\\s"'\`),;:!?\\]]|\\.(?=\\s|$))`;
|
|
321
|
+
return new RegExp(boundary, "u").test(advice);
|
|
309
322
|
});
|
|
310
323
|
if (!mentioned) {
|
|
311
324
|
return false;
|
|
@@ -354,7 +367,7 @@ export class AdvisorSessionState {
|
|
|
354
367
|
(item) => item.kind === "gate"
|
|
355
368
|
);
|
|
356
369
|
return (
|
|
357
|
-
(["proceed", "revise", "blocked"] as
|
|
370
|
+
(["proceed", "revise", "blocked"] as const)
|
|
358
371
|
.map(
|
|
359
372
|
(decision) =>
|
|
360
373
|
[
|
|
@@ -375,15 +388,17 @@ export class AdvisorSessionState {
|
|
|
375
388
|
|
|
376
389
|
#triggersLine() {
|
|
377
390
|
return (
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
391
|
+
(
|
|
392
|
+
[
|
|
393
|
+
"manual",
|
|
394
|
+
"executor-requested",
|
|
395
|
+
"turn-gate",
|
|
396
|
+
"repeated-tool-call",
|
|
397
|
+
"completion-review",
|
|
398
|
+
"custom-rule",
|
|
399
|
+
] as const
|
|
400
|
+
)
|
|
401
|
+
.filter((trigger) => this.#countTrigger(trigger) > 0)
|
|
387
402
|
.join(", ") || "none"
|
|
388
403
|
);
|
|
389
404
|
}
|
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
|
};
|