pi-advisor-flow 0.8.3 → 0.9.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 +7 -0
- package/README.md +5 -4
- package/dist/index.js +631 -76
- package/package.json +1 -1
- package/src/attachments.ts +21 -5
- package/src/commands/activation.ts +6 -1
- package/src/commands/lifecycle.ts +6 -0
- package/src/commands/manual-command.ts +13 -1
- package/src/commands/model-commands.ts +1 -0
- package/src/commands/runtime.ts +29 -0
- package/src/commands/settings-commands.ts +2 -0
- package/src/commands/settings-persistence.ts +2 -0
- package/src/commands/types.ts +5 -0
- package/src/config/defaults.ts +7 -0
- package/src/config/schema.ts +7 -0
- package/src/config/state.ts +5 -0
- package/src/config/types.ts +1 -0
- package/src/conversation.ts +29 -7
- package/src/image-attachments.ts +118 -0
- package/src/image-validation.ts +288 -0
- package/src/images.ts +151 -0
- package/src/scout-context.ts +1 -0
- package/src/scout-curation.ts +11 -4
- package/src/scout-groups.ts +13 -3
- package/src/scout-types.ts +2 -0
- package/src/tools/consult-context.ts +78 -2
- package/src/tools/consultation.ts +36 -4
- package/src/tools/jev-turn-gate.ts +4 -1
- package/src/tools/model-access.ts +21 -2
- package/src/tools/prompts.ts +14 -2
- package/src/tools/register-ask-advisor.ts +28 -1
- package/src/tools/register-lifecycle.ts +8 -2
- package/src/tools/render-advisor-result.ts +24 -8
- package/src/tools/types.ts +9 -0
- package/src/ui/settings-items.ts +17 -1
- package/src/ui/settings-mutations.ts +1 -0
- package/src/ui/types.ts +1 -0
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
advisorRef,
|
|
10
10
|
} from "../config/state.ts";
|
|
11
11
|
import { loadConfig } from "../config/storage.ts";
|
|
12
|
+
import { escapeRepositoryText } from "../git.ts";
|
|
12
13
|
import type { GitContextLevel } from "../git.ts";
|
|
13
14
|
import { collectTextStream, resolveConfiguredModel } from "../model-stream.ts";
|
|
14
15
|
import { redactSecrets } from "../redaction.ts";
|
|
@@ -16,7 +17,11 @@ import type { ScoutLifecycleEvent } from "../scout.ts";
|
|
|
16
17
|
import type { ConsultationTrigger, GateTrigger } from "../session-state.ts";
|
|
17
18
|
import { assembleConsultationContext } from "./consult-context.ts";
|
|
18
19
|
import { parseAutomaticDecision } from "./gate-protocol.ts";
|
|
19
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
advisorModelAccessReason,
|
|
22
|
+
sameModelAdvisorDisabled,
|
|
23
|
+
sameModelAdvisorNotice,
|
|
24
|
+
} from "./model-access.ts";
|
|
20
25
|
import {
|
|
21
26
|
ADVISOR_DECISION_SYSTEM,
|
|
22
27
|
ADVISOR_SYSTEM,
|
|
@@ -57,12 +62,18 @@ const collectAdvisorResponse = async (
|
|
|
57
62
|
) => {
|
|
58
63
|
const { ctx, question, signal, systemPrompt } = options;
|
|
59
64
|
loadConfig(ctx);
|
|
65
|
+
if (sameModelAdvisorDisabled(ctx)) {
|
|
66
|
+
throw new Error(sameModelAdvisorNotice);
|
|
67
|
+
}
|
|
60
68
|
const accessReason = advisorModelAccessReason(ctx);
|
|
61
69
|
if (accessReason) {
|
|
62
70
|
throw new Error(accessReason);
|
|
63
71
|
}
|
|
64
72
|
const resolved = await resolveConfiguredModel(ctx, advisorRef, "Advisor");
|
|
65
|
-
const context = await assembleConsultationContext(
|
|
73
|
+
const context = await assembleConsultationContext({
|
|
74
|
+
...options,
|
|
75
|
+
supportsImages: resolved.model.input.includes("image"),
|
|
76
|
+
});
|
|
66
77
|
|
|
67
78
|
// Keep the original question available to local UI callers, but never send
|
|
68
79
|
// its credential-shaped values to the provider when redaction is enabled.
|
|
@@ -70,11 +81,17 @@ const collectAdvisorResponse = async (
|
|
|
70
81
|
advisorRedactSecretsRef && question !== undefined
|
|
71
82
|
? redactSecrets(question)
|
|
72
83
|
: question;
|
|
84
|
+
const imageNotice =
|
|
85
|
+
context.imageOmissions ||
|
|
86
|
+
context.images.length ||
|
|
87
|
+
/\[Image[ :]/u.test(context.conversation)
|
|
88
|
+
? `\n\nImage disclosure: ${context.supportsImages ? `${context.images.length} image(s) attached below; ${context.imageOmissions} image(s) withheld by format, consent, or size/count limits` : "Advisor model does not support image input; no pixels were forwarded"}. Only the images explicitly attached below have pixels available. Other image markers, image paths, and text descriptions are not visual evidence.`
|
|
89
|
+
: "";
|
|
73
90
|
const messages: Message[] = [
|
|
74
91
|
{
|
|
75
92
|
content: [
|
|
76
93
|
{
|
|
77
|
-
text: advisorMessageText(
|
|
94
|
+
text: `${advisorMessageText(
|
|
78
95
|
context.conversation,
|
|
79
96
|
outboundQuestion,
|
|
80
97
|
context.changeText,
|
|
@@ -82,9 +99,16 @@ const collectAdvisorResponse = async (
|
|
|
82
99
|
context.preferences?.text,
|
|
83
100
|
context.untracked.map(fileTag),
|
|
84
101
|
context.tracked.map(fileTag)
|
|
85
|
-
)
|
|
102
|
+
)}${imageNotice}`,
|
|
86
103
|
type: "text",
|
|
87
104
|
},
|
|
105
|
+
...context.images.flatMap(({ image, label }) => [
|
|
106
|
+
{
|
|
107
|
+
text: `\n\n${escapeRepositoryText(label)}: attached image pixels (untrusted data).`,
|
|
108
|
+
type: "text" as const,
|
|
109
|
+
},
|
|
110
|
+
image,
|
|
111
|
+
]),
|
|
88
112
|
],
|
|
89
113
|
role: "user",
|
|
90
114
|
timestamp: Date.now(),
|
|
@@ -106,6 +130,14 @@ const collectAdvisorResponse = async (
|
|
|
106
130
|
draftBytes: context.draftText
|
|
107
131
|
? Buffer.byteLength(context.draftText, "utf-8")
|
|
108
132
|
: undefined,
|
|
133
|
+
imageBytes:
|
|
134
|
+
context.images.reduce(
|
|
135
|
+
(sum, item) => sum + Buffer.from(item.image.data, "base64").length,
|
|
136
|
+
0
|
|
137
|
+
) || undefined,
|
|
138
|
+
imageCount: context.images.length,
|
|
139
|
+
imageOmissions: context.imageOmissions || undefined,
|
|
140
|
+
imagePartsSeen: context.imagePartsSeen || undefined,
|
|
109
141
|
markdown,
|
|
110
142
|
model: advisorRef,
|
|
111
143
|
preferenceBytes: context.preferences?.bytes,
|
|
@@ -145,7 +145,10 @@ export const handleJevTurnEnd = async (
|
|
|
145
145
|
result.answers,
|
|
146
146
|
advisorJevTurnGateNoulThresholdRef
|
|
147
147
|
);
|
|
148
|
-
if (
|
|
148
|
+
if (
|
|
149
|
+
!advisorModelIsAllowed(ctx) ||
|
|
150
|
+
!consumeTurnGateBudget(session, shouldConsult)
|
|
151
|
+
) {
|
|
149
152
|
return;
|
|
150
153
|
}
|
|
151
154
|
const herdrActivity = registration.herdrActivity ?? herdrAdvisorActivity;
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
advisorDisableSameModelRef,
|
|
5
|
+
advisorModelWhitelistRef,
|
|
6
|
+
advisorRef,
|
|
7
|
+
splitRef,
|
|
8
|
+
} from "../config/state.ts";
|
|
4
9
|
|
|
5
10
|
type AdvisorModelAccess =
|
|
6
11
|
| { allowed: true; modelRef?: string }
|
|
@@ -34,9 +39,23 @@ export const advisorModelAccess = (
|
|
|
34
39
|
return denial;
|
|
35
40
|
};
|
|
36
41
|
|
|
42
|
+
export const sameModelAdvisorDisabled = (
|
|
43
|
+
ctx: Pick<ExtensionContext, "model">,
|
|
44
|
+
model: { id: string; provider: string } | undefined = ctx.model
|
|
45
|
+
): boolean => {
|
|
46
|
+
if (!(advisorDisableSameModelRef && advisorRef && model)) {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
const [provider, id] = splitRef(advisorRef);
|
|
50
|
+
return provider === model.provider && id === model.id;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export const sameModelAdvisorNotice =
|
|
54
|
+
"Advisor disabled: executor and advisor are the same model.";
|
|
55
|
+
|
|
37
56
|
export const advisorModelIsAllowed = (
|
|
38
57
|
ctx: Pick<ExtensionContext, "model">
|
|
39
|
-
): boolean => advisorModelAccess(ctx).allowed;
|
|
58
|
+
): boolean => advisorModelAccess(ctx).allowed && !sameModelAdvisorDisabled(ctx);
|
|
40
59
|
|
|
41
60
|
export const advisorModelAccessReason = (
|
|
42
61
|
ctx: Pick<ExtensionContext, "model">
|
package/src/tools/prompts.ts
CHANGED
|
@@ -97,8 +97,20 @@ export const advisorRepositoryContext = (
|
|
|
97
97
|
*/
|
|
98
98
|
export const advisorRequestConversation = (
|
|
99
99
|
ctx: ExtensionContext,
|
|
100
|
-
maxChars = contextMaxCharsRef
|
|
101
|
-
|
|
100
|
+
maxChars = contextMaxCharsRef,
|
|
101
|
+
describeImages = false,
|
|
102
|
+
imageNonce = ""
|
|
103
|
+
) =>
|
|
104
|
+
recentConversation(
|
|
105
|
+
ctx,
|
|
106
|
+
maxChars,
|
|
107
|
+
undefined,
|
|
108
|
+
undefined,
|
|
109
|
+
undefined,
|
|
110
|
+
undefined,
|
|
111
|
+
describeImages,
|
|
112
|
+
imageNonce
|
|
113
|
+
);
|
|
102
114
|
|
|
103
115
|
export const advisorInvocationGuidelines = () => {
|
|
104
116
|
if (isSimpleMode()) {
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
getAdvisorSettings,
|
|
11
11
|
isSimpleMode,
|
|
12
12
|
} from "../config/state.ts";
|
|
13
|
+
import { loadConfig } from "../config/storage.ts";
|
|
13
14
|
import { notifyHerdrAdvisorFailure } from "../herdr.ts";
|
|
14
15
|
import {
|
|
15
16
|
ADVISOR_STREAM_UPDATE_INTERVAL_MS,
|
|
@@ -22,7 +23,11 @@ import {
|
|
|
22
23
|
} from "../usage.ts";
|
|
23
24
|
import { notifyLocalFailure, updateAdvisorUsageStatus } from "./gate-policy.ts";
|
|
24
25
|
import { normalizeScreeningQuestion, screeningSkipText } from "./jev-filter.ts";
|
|
25
|
-
import {
|
|
26
|
+
import {
|
|
27
|
+
advisorModelAccessReason,
|
|
28
|
+
sameModelAdvisorDisabled,
|
|
29
|
+
sameModelAdvisorNotice,
|
|
30
|
+
} from "./model-access.ts";
|
|
26
31
|
import { renderAdvisorResult } from "./render-advisor-result.ts";
|
|
27
32
|
import {
|
|
28
33
|
renderAdvisorCallBox,
|
|
@@ -73,7 +78,22 @@ export const registerAskAdvisorTool = ({
|
|
|
73
78
|
description:
|
|
74
79
|
"Consult the on-demand Advisor model for strategic guidance. Call with an empty object for a contextual review; attach an optional draft for concrete plan or completion review. If the Advisor explicitly names a missing file, you may make a sequential follow-up call with includeTrackedFiles when enabled and relevant.",
|
|
75
80
|
async execute(_id, params, signal, onUpdate, ctx) {
|
|
81
|
+
const skipSameModel = () => {
|
|
82
|
+
session.releaseCall(_id);
|
|
83
|
+
reservedCalls.delete(_id);
|
|
84
|
+
return {
|
|
85
|
+
content: [{ text: sameModelAdvisorNotice, type: "text" as const }],
|
|
86
|
+
details: {
|
|
87
|
+
skipReason: sameModelAdvisorNotice,
|
|
88
|
+
text: sameModelAdvisorNotice,
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
};
|
|
76
92
|
try {
|
|
93
|
+
loadConfig(ctx);
|
|
94
|
+
if (sameModelAdvisorDisabled(ctx)) {
|
|
95
|
+
return skipSameModel();
|
|
96
|
+
}
|
|
77
97
|
assertAdvisorModelAccess(ctx);
|
|
78
98
|
} catch (error) {
|
|
79
99
|
session.releaseCall(_id);
|
|
@@ -128,6 +148,9 @@ export const registerAskAdvisorTool = ({
|
|
|
128
148
|
},
|
|
129
149
|
};
|
|
130
150
|
}
|
|
151
|
+
if (sameModelAdvisorDisabled(ctx)) {
|
|
152
|
+
return skipSameModel();
|
|
153
|
+
}
|
|
131
154
|
if (
|
|
132
155
|
!simpleMode &&
|
|
133
156
|
!session.canConsult(getAdvisorMaxCallsPerSession(), _id)
|
|
@@ -217,6 +240,10 @@ export const registerAskAdvisorTool = ({
|
|
|
217
240
|
adviceId: result.adviceId,
|
|
218
241
|
advisor: result.model,
|
|
219
242
|
draftBytes: result.draftBytes,
|
|
243
|
+
imageBytes: result.imageBytes,
|
|
244
|
+
imageCount: result.imageCount,
|
|
245
|
+
imageOmissions: result.imageOmissions,
|
|
246
|
+
imagePartsSeen: result.imagePartsSeen,
|
|
220
247
|
preferenceBytes: result.preferenceBytes,
|
|
221
248
|
question: resolveAdvisorRequest(params.question),
|
|
222
249
|
scout: scoutDetails,
|
|
@@ -6,7 +6,10 @@ import {
|
|
|
6
6
|
import { loadConfig } from "../config/storage.ts";
|
|
7
7
|
import { reserveAdvisorCall } from "./gate-policy.ts";
|
|
8
8
|
import { handleAutomaticGate } from "./loop-gate.ts";
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
advisorModelAccess,
|
|
11
|
+
sameModelAdvisorDisabled,
|
|
12
|
+
} from "./model-access.ts";
|
|
10
13
|
import { advisorInvocationGuidelines } from "./prompts.ts";
|
|
11
14
|
import type { ToolRegistrationContext } from "./types.ts";
|
|
12
15
|
|
|
@@ -73,7 +76,7 @@ export const registerToolLifecycle = ({
|
|
|
73
76
|
return;
|
|
74
77
|
}
|
|
75
78
|
loadConfig(ctx);
|
|
76
|
-
if (!advisorModelAccess(ctx).allowed) {
|
|
79
|
+
if (!advisorModelAccess(ctx).allowed || sameModelAdvisorDisabled(ctx)) {
|
|
77
80
|
return;
|
|
78
81
|
}
|
|
79
82
|
const guidelines = advisorInvocationGuidelines();
|
|
@@ -107,6 +110,9 @@ export const registerToolLifecycle = ({
|
|
|
107
110
|
// tool errors; every other tool must proceed without Advisor gating.
|
|
108
111
|
return;
|
|
109
112
|
}
|
|
113
|
+
if (sameModelAdvisorDisabled(ctx)) {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
110
116
|
const accessBlock = modelAccessBlock(event.toolName, ctx);
|
|
111
117
|
if (accessBlock) {
|
|
112
118
|
return accessBlock;
|
|
@@ -30,23 +30,39 @@ const syncRenderPhase = (context: AdvisorToolContext, phase: string) => {
|
|
|
30
30
|
context.state.phase = phase;
|
|
31
31
|
};
|
|
32
32
|
|
|
33
|
+
const formatByteSize = (bytes: number): string => {
|
|
34
|
+
if (bytes >= 1024 ** 3) {
|
|
35
|
+
return `${(bytes / 1024 ** 3).toFixed(1)} GiB`;
|
|
36
|
+
}
|
|
37
|
+
if (bytes >= 1024 ** 2) {
|
|
38
|
+
return `${(bytes / 1024 ** 2).toFixed(1)} MiB`;
|
|
39
|
+
}
|
|
40
|
+
if (bytes >= 1024) {
|
|
41
|
+
return `${Math.round(bytes / 1024)} KiB`;
|
|
42
|
+
}
|
|
43
|
+
return `${bytes} B`;
|
|
44
|
+
};
|
|
45
|
+
|
|
33
46
|
const attachmentLabels = (details: AdvisorToolDetails | undefined) =>
|
|
34
47
|
[
|
|
35
48
|
details?.draftBytes
|
|
36
|
-
? `Draft attached · ${details.draftBytes}
|
|
49
|
+
? `Draft attached · ${formatByteSize(details.draftBytes)}`
|
|
37
50
|
: undefined,
|
|
38
51
|
details?.preferenceBytes
|
|
39
|
-
? `Project preferences attached · ${details.preferenceBytes}
|
|
52
|
+
? `Project preferences attached · ${formatByteSize(details.preferenceBytes)}`
|
|
40
53
|
: undefined,
|
|
41
54
|
details?.trackedBytes
|
|
42
|
-
? `Tracked files attached · ${details.trackedBytes}
|
|
55
|
+
? `Tracked files attached · ${formatByteSize(details.trackedBytes)}`
|
|
43
56
|
: undefined,
|
|
44
57
|
details?.untrackedBytes
|
|
45
|
-
? `Untracked files attached · ${details.untrackedBytes}
|
|
58
|
+
? `Untracked files attached · ${formatByteSize(details.untrackedBytes)}`
|
|
59
|
+
: undefined,
|
|
60
|
+
details?.imageCount
|
|
61
|
+
? `${details.imageCount} image${details.imageCount === 1 ? "" : "s"} attached${details.imageBytes ? ` · ${formatByteSize(details.imageBytes)}` : ""}${details.imageOmissions ? ` · ${details.imageOmissions} withheld` : ""}`
|
|
46
62
|
: undefined,
|
|
47
63
|
].filter((label): label is string => label !== undefined);
|
|
48
64
|
|
|
49
|
-
const
|
|
65
|
+
const renderSkipBox = (
|
|
50
66
|
box: Box,
|
|
51
67
|
result: AgentToolResult<AdvisorToolDetails>,
|
|
52
68
|
expanded: boolean,
|
|
@@ -55,7 +71,7 @@ const renderJevSkipBox = (
|
|
|
55
71
|
const details = advisorResultDetails(result);
|
|
56
72
|
const lines = [
|
|
57
73
|
theme.fg("dim", theme.bold("◆ ADVISOR · SKIPPED")),
|
|
58
|
-
theme.fg("dim", ` ${details?.jev?.reason ?? ""}`),
|
|
74
|
+
theme.fg("dim", ` ${details?.skipReason ?? details?.jev?.reason ?? ""}`),
|
|
59
75
|
];
|
|
60
76
|
box.addChild(new Text(lines.join("\n"), 0, 0));
|
|
61
77
|
box.addChild(
|
|
@@ -157,8 +173,8 @@ const renderFinalAdvisorResult = (
|
|
|
157
173
|
context.state.timerId = undefined;
|
|
158
174
|
}
|
|
159
175
|
const details = advisorResultDetails(result);
|
|
160
|
-
if (details?.jev?.skipped) {
|
|
161
|
-
|
|
176
|
+
if (details?.jev?.skipped || details?.skipReason) {
|
|
177
|
+
renderSkipBox(box, result, expanded, theme);
|
|
162
178
|
return;
|
|
163
179
|
}
|
|
164
180
|
if (details?.scout) {
|
package/src/tools/types.ts
CHANGED
|
@@ -47,6 +47,10 @@ export interface AdvisorGateFailure {
|
|
|
47
47
|
export interface AdvisorConsultationResult {
|
|
48
48
|
adviceId: string;
|
|
49
49
|
draftBytes?: number;
|
|
50
|
+
imageBytes?: number;
|
|
51
|
+
imageCount?: number;
|
|
52
|
+
imageOmissions?: number;
|
|
53
|
+
imagePartsSeen?: number;
|
|
50
54
|
markdown: string;
|
|
51
55
|
model: string;
|
|
52
56
|
preferenceBytes?: number;
|
|
@@ -83,6 +87,10 @@ export interface AdvisorToolDetails {
|
|
|
83
87
|
adviceId?: string;
|
|
84
88
|
advisor?: string;
|
|
85
89
|
draftBytes?: number;
|
|
90
|
+
imageBytes?: number;
|
|
91
|
+
imageCount?: number;
|
|
92
|
+
imageOmissions?: number;
|
|
93
|
+
imagePartsSeen?: number;
|
|
86
94
|
jev?: {
|
|
87
95
|
kind: JevSkipKind;
|
|
88
96
|
reason: string;
|
|
@@ -91,6 +99,7 @@ export interface AdvisorToolDetails {
|
|
|
91
99
|
preferenceBytes?: number;
|
|
92
100
|
question?: string;
|
|
93
101
|
scout?: ScoutToolDetails;
|
|
102
|
+
skipReason?: string;
|
|
94
103
|
text?: string;
|
|
95
104
|
thinking?: string;
|
|
96
105
|
trackedBytes?: number;
|
package/src/ui/settings-items.ts
CHANGED
|
@@ -284,7 +284,16 @@ export const createSettingsItems = ({
|
|
|
284
284
|
},
|
|
285
285
|
];
|
|
286
286
|
if (settings.simpleMode) {
|
|
287
|
-
items.push(
|
|
287
|
+
items.push(
|
|
288
|
+
modelWhitelist,
|
|
289
|
+
toggle(
|
|
290
|
+
"disableSameModel",
|
|
291
|
+
"Disable same-model Advisor",
|
|
292
|
+
"Skip advice when the active Executor and Advisor use the same provider/model; turn off to allow higher-effort same-model reviews.",
|
|
293
|
+
settings.disableSameModel,
|
|
294
|
+
true
|
|
295
|
+
)
|
|
296
|
+
);
|
|
288
297
|
return items;
|
|
289
298
|
}
|
|
290
299
|
|
|
@@ -297,6 +306,13 @@ export const createSettingsItems = ({
|
|
|
297
306
|
values: withCurrentValue(currentEffort(settings.effort), effortLevels),
|
|
298
307
|
},
|
|
299
308
|
modelWhitelist,
|
|
309
|
+
toggle(
|
|
310
|
+
"disableSameModel",
|
|
311
|
+
"Disable same-model Advisor",
|
|
312
|
+
"Skip advice when the active Executor and Advisor use the same provider/model; turn off to allow higher-effort same-model reviews.",
|
|
313
|
+
settings.disableSameModel,
|
|
314
|
+
true
|
|
315
|
+
),
|
|
300
316
|
toggle(
|
|
301
317
|
"scoutEnabled",
|
|
302
318
|
"Experimental Advisor Scout",
|