pi-advisor-flow 0.9.2-dev.5 → 0.10.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 +6 -0
- package/README.md +9 -5
- package/dist/index.js +404 -65
- package/package.json +2 -2
- package/src/commands/activation-preparation.ts +4 -0
- package/src/commands/manual-consultation.ts +41 -29
- package/src/commands/model-commands.ts +5 -1
- package/src/commands/model-picker.ts +44 -2
- package/src/commands/runtime.ts +9 -1
- package/src/commands/settings-persistence.ts +2 -0
- package/src/commands/types.ts +2 -0
- package/src/config/defaults.ts +6 -0
- package/src/config/schema.ts +7 -0
- package/src/config/state.ts +5 -0
- package/src/config/types.ts +1 -0
- package/src/config.ts +2 -0
- package/src/follow-up.ts +85 -0
- package/src/session-state.ts +33 -0
- package/src/tools/consultation.ts +311 -41
- package/src/tools/model-access.ts +7 -3
- package/src/tools/register-ask-advisor.ts +109 -23
- package/src/tools/register-lifecycle.ts +11 -0
- package/src/tools/render-advisor-result.ts +3 -0
- package/src/tools/render-common.ts +6 -2
- package/src/tools/types.ts +2 -0
- package/src/ui/settings-items.ts +36 -0
- package/src/ui/settings-mutations.ts +8 -0
- package/src/ui/settings-selector.ts +9 -0
- package/src/ui/types.ts +3 -0
|
@@ -5,10 +5,13 @@ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
|
5
5
|
|
|
6
6
|
import {
|
|
7
7
|
advisorEffortRef,
|
|
8
|
+
advisorFallbackModelRef,
|
|
8
9
|
advisorRedactSecretsRef,
|
|
9
10
|
advisorRef,
|
|
11
|
+
getAdvisorSettings,
|
|
10
12
|
} from "../config/state.ts";
|
|
11
13
|
import { loadConfig } from "../config/storage.ts";
|
|
14
|
+
import type { AdvisorFollowUpPayload } from "../follow-up.ts";
|
|
12
15
|
import { escapeRepositoryText } from "../git.ts";
|
|
13
16
|
import type { GitContextLevel } from "../git.ts";
|
|
14
17
|
import { collectTextStream, resolveConfiguredModel } from "../model-stream.ts";
|
|
@@ -20,6 +23,7 @@ import type { ConsultationContext } from "./consult-context.ts";
|
|
|
20
23
|
import { parseAutomaticDecision } from "./gate-protocol.ts";
|
|
21
24
|
import {
|
|
22
25
|
advisorModelAccessReason,
|
|
26
|
+
fallbackSameModelAdvisorNotice,
|
|
23
27
|
sameModelAdvisorDisabled,
|
|
24
28
|
sameModelAdvisorNotice,
|
|
25
29
|
} from "./model-access.ts";
|
|
@@ -41,6 +45,17 @@ class AdvisorNoAdviceError extends Error {
|
|
|
41
45
|
}
|
|
42
46
|
}
|
|
43
47
|
|
|
48
|
+
export interface AdvisorFollowUpRequest {
|
|
49
|
+
adviceId: string;
|
|
50
|
+
payload: AdvisorFollowUpPayload;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export type AdvisorFollowUpCapture = (
|
|
54
|
+
adviceId: string,
|
|
55
|
+
payload: AdvisorFollowUpPayload,
|
|
56
|
+
replacesAdviceId?: string
|
|
57
|
+
) => void;
|
|
58
|
+
|
|
44
59
|
interface CollectAdvisorResponseOptions {
|
|
45
60
|
ctx: ExtensionContext;
|
|
46
61
|
currentInvocationId?: string;
|
|
@@ -53,8 +68,22 @@ interface CollectAdvisorResponseOptions {
|
|
|
53
68
|
question?: string;
|
|
54
69
|
signal?: AbortSignal;
|
|
55
70
|
systemPrompt: string;
|
|
71
|
+
followUp?: AdvisorFollowUpRequest;
|
|
56
72
|
}
|
|
57
73
|
|
|
74
|
+
interface PreparedAdvisorRequest {
|
|
75
|
+
context?: ConsultationContext;
|
|
76
|
+
messages: Message[];
|
|
77
|
+
privacyKey: string;
|
|
78
|
+
redacted: boolean;
|
|
79
|
+
systemPrompt: string;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
type CollectedAdvisorResponse = Omit<
|
|
83
|
+
AdvisorConsultationResult,
|
|
84
|
+
"adviceId" | "trigger"
|
|
85
|
+
> & { followUpPayload: AdvisorFollowUpPayload };
|
|
86
|
+
|
|
58
87
|
const fileTag = (item: { path: string; text: string }) =>
|
|
59
88
|
`<file path=${JSON.stringify(item.path)}>\n${item.text}\n</file>`;
|
|
60
89
|
|
|
@@ -86,26 +115,79 @@ const advisorImageNotice = (context: ConsultationContext) => {
|
|
|
86
115
|
return `\n\nImage disclosure: ${disclosure}. Only the images explicitly attached below have pixels available. Other image markers, image paths, and text descriptions are not visual evidence.`;
|
|
87
116
|
};
|
|
88
117
|
|
|
89
|
-
const
|
|
90
|
-
|
|
118
|
+
const advisorPrivacyKey = (ctx: ExtensionContext) => {
|
|
119
|
+
const settings = getAdvisorSettings();
|
|
120
|
+
return JSON.stringify({
|
|
121
|
+
agentsMdContext: settings.agentsMdContext,
|
|
122
|
+
contextMaxChars: settings.contextMaxChars,
|
|
123
|
+
cwd: ctx.cwd,
|
|
124
|
+
gitContext: settings.gitContext,
|
|
125
|
+
gitContextMaxChars: settings.gitContextMaxChars,
|
|
126
|
+
projectTrusted: ctx.isProjectTrusted(),
|
|
127
|
+
redactSecrets: settings.redactSecrets,
|
|
128
|
+
scoutEnabled: settings.scoutEnabled,
|
|
129
|
+
toolPolicies: settings.toolPolicies,
|
|
130
|
+
toolResultMaxBytes: settings.toolResultMaxBytes,
|
|
131
|
+
toolResultMaxLines: settings.toolResultMaxLines,
|
|
132
|
+
trackedFileContent: settings.trackedFileContent,
|
|
133
|
+
untrackedContent: settings.untrackedContent,
|
|
134
|
+
});
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
export const assertAdvisorFollowUpPrivacy = (
|
|
138
|
+
payload: AdvisorFollowUpPayload,
|
|
139
|
+
ctx: ExtensionContext
|
|
91
140
|
) => {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
141
|
+
if (payload.privacyKey !== advisorPrivacyKey(ctx)) {
|
|
142
|
+
throw new Error(
|
|
143
|
+
"Follow-up consultation unavailable: privacy or disclosure settings changed after the original consultation. Issue a fresh consultation."
|
|
144
|
+
);
|
|
96
145
|
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const prepareFollowUp = (
|
|
149
|
+
followUp: AdvisorFollowUpRequest,
|
|
150
|
+
question: string | undefined,
|
|
151
|
+
ctx: ExtensionContext
|
|
152
|
+
): PreparedAdvisorRequest => {
|
|
153
|
+
if (!question?.trim()) {
|
|
154
|
+
throw new Error(
|
|
155
|
+
"Follow-up consultations require a non-empty question. Issue a fresh consultation if no question is needed."
|
|
156
|
+
);
|
|
100
157
|
}
|
|
101
|
-
|
|
158
|
+
assertAdvisorFollowUpPrivacy(followUp.payload, ctx);
|
|
159
|
+
const outboundQuestion =
|
|
160
|
+
advisorRedactSecretsRef || followUp.payload.redacted
|
|
161
|
+
? redactSecrets(question)
|
|
162
|
+
: question;
|
|
163
|
+
const messages = structuredClone(followUp.payload.messages);
|
|
164
|
+
messages.push({
|
|
165
|
+
content: [
|
|
166
|
+
{
|
|
167
|
+
text: `Follow-up focus:\n${outboundQuestion}`,
|
|
168
|
+
type: "text",
|
|
169
|
+
},
|
|
170
|
+
],
|
|
171
|
+
role: "user",
|
|
172
|
+
timestamp: Date.now(),
|
|
173
|
+
});
|
|
174
|
+
return {
|
|
175
|
+
messages,
|
|
176
|
+
privacyKey: followUp.payload.privacyKey,
|
|
177
|
+
redacted: advisorRedactSecretsRef || followUp.payload.redacted,
|
|
178
|
+
systemPrompt: followUp.payload.systemPrompt,
|
|
179
|
+
};
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
const prepareFreshRequest = async (
|
|
183
|
+
options: CollectAdvisorResponseOptions,
|
|
184
|
+
resolved: Awaited<ReturnType<typeof resolveConfiguredModel>>
|
|
185
|
+
): Promise<PreparedAdvisorRequest> => {
|
|
186
|
+
const { question } = options;
|
|
102
187
|
const context = await assembleConsultationContext({
|
|
103
188
|
...options,
|
|
104
189
|
supportsImages: resolved.model.input.includes("image"),
|
|
105
190
|
});
|
|
106
|
-
|
|
107
|
-
// Keep the original question available to local UI callers, but never send
|
|
108
|
-
// its credential-shaped values to the provider when redaction is enabled.
|
|
109
191
|
const outboundQuestion =
|
|
110
192
|
advisorRedactSecretsRef && question !== undefined
|
|
111
193
|
? redactSecrets(question)
|
|
@@ -139,45 +221,227 @@ const collectAdvisorResponse = async (
|
|
|
139
221
|
timestamp: Date.now(),
|
|
140
222
|
},
|
|
141
223
|
];
|
|
224
|
+
return {
|
|
225
|
+
context,
|
|
226
|
+
messages,
|
|
227
|
+
privacyKey: advisorPrivacyKey(options.ctx),
|
|
228
|
+
redacted: advisorRedactSecretsRef,
|
|
229
|
+
systemPrompt: options.systemPrompt,
|
|
230
|
+
};
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
const combinedFallbackError = (
|
|
234
|
+
primaryModel: string,
|
|
235
|
+
primaryError: Error,
|
|
236
|
+
fallbackModel: string,
|
|
237
|
+
fallbackError: Error
|
|
238
|
+
) =>
|
|
239
|
+
new Error(
|
|
240
|
+
`Advisor model ${primaryModel} failed: ${primaryError.message}. Fallback Advisor model ${fallbackModel} failed: ${fallbackError.message}.`,
|
|
241
|
+
{ cause: fallbackError }
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
const advisorContextDetails = (context: ConsultationContext | undefined) => ({
|
|
245
|
+
agentRulesBytes: context?.projectRules?.bytes || undefined,
|
|
246
|
+
draftBytes: context?.draftText
|
|
247
|
+
? Buffer.byteLength(context.draftText, "utf-8")
|
|
248
|
+
: undefined,
|
|
249
|
+
imageBytes:
|
|
250
|
+
context?.images.reduce(
|
|
251
|
+
(sum, item) => sum + Buffer.from(item.image.data, "base64").length,
|
|
252
|
+
0
|
|
253
|
+
) || undefined,
|
|
254
|
+
imageCount: context?.images.length,
|
|
255
|
+
imageOmissions: context?.imageOmissions || undefined,
|
|
256
|
+
imagePartsSeen: context?.imagePartsSeen || undefined,
|
|
257
|
+
preferenceBytes: context?.preferences?.bytes,
|
|
258
|
+
scout: context?.scout,
|
|
259
|
+
trackedBytes:
|
|
260
|
+
context?.tracked.reduce((sum, item) => sum + item.bytes, 0) || undefined,
|
|
261
|
+
untrackedBytes:
|
|
262
|
+
context?.untracked.reduce((sum, item) => sum + item.bytes, 0) || undefined,
|
|
263
|
+
});
|
|
142
264
|
|
|
265
|
+
const runAdvisorAttempt = async (
|
|
266
|
+
options: CollectAdvisorResponseOptions,
|
|
267
|
+
prepared: PreparedAdvisorRequest,
|
|
268
|
+
resolved: Awaited<ReturnType<typeof resolveConfiguredModel>>,
|
|
269
|
+
modelRef: string
|
|
270
|
+
): Promise<CollectedAdvisorResponse> => {
|
|
143
271
|
const streamed = await collectTextStream(resolved, {
|
|
144
|
-
messages,
|
|
272
|
+
messages: structuredClone(prepared.messages),
|
|
145
273
|
onChunk: options.onChunk,
|
|
146
274
|
reasoning: advisorEffortRef,
|
|
147
|
-
signal,
|
|
148
|
-
systemPrompt,
|
|
275
|
+
signal: options.signal,
|
|
276
|
+
systemPrompt: prepared.systemPrompt,
|
|
149
277
|
});
|
|
150
|
-
const markdown = streamed
|
|
278
|
+
const { text: markdown } = streamed;
|
|
151
279
|
if (!markdown.trim()) {
|
|
152
280
|
throw new AdvisorNoAdviceError();
|
|
153
281
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
:
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
imageOmissions: context.imageOmissions || undefined,
|
|
166
|
-
imagePartsSeen: context.imagePartsSeen || undefined,
|
|
282
|
+
return {
|
|
283
|
+
...advisorContextDetails(prepared.context),
|
|
284
|
+
followUp: Boolean(options.followUp),
|
|
285
|
+
followUpPayload: {
|
|
286
|
+
depth: options.followUp ? options.followUp.payload.depth + 1 : 0,
|
|
287
|
+
messages: structuredClone(prepared.messages),
|
|
288
|
+
model: modelRef,
|
|
289
|
+
privacyKey: prepared.privacyKey,
|
|
290
|
+
redacted: prepared.redacted,
|
|
291
|
+
systemPrompt: prepared.systemPrompt,
|
|
292
|
+
},
|
|
167
293
|
markdown,
|
|
168
|
-
model:
|
|
169
|
-
preferenceBytes: context.preferences?.bytes,
|
|
294
|
+
model: modelRef,
|
|
170
295
|
thinkingText: streamed.thinking,
|
|
171
|
-
trackedBytes:
|
|
172
|
-
context.tracked.reduce((sum, item) => sum + item.bytes, 0) || undefined,
|
|
173
|
-
untrackedBytes:
|
|
174
|
-
context.untracked.reduce((sum, item) => sum + item.bytes, 0) || undefined,
|
|
175
296
|
usage: streamed.usage,
|
|
176
297
|
};
|
|
177
|
-
|
|
178
|
-
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
const runFallbackAdvisorAttempt = async (
|
|
301
|
+
options: CollectAdvisorResponseOptions,
|
|
302
|
+
primaryModel: string,
|
|
303
|
+
primaryError: Error,
|
|
304
|
+
prepared: PreparedAdvisorRequest | undefined
|
|
305
|
+
): Promise<CollectedAdvisorResponse> => {
|
|
306
|
+
const { ctx } = options;
|
|
307
|
+
let request = prepared;
|
|
308
|
+
if (!advisorFallbackModelRef) {
|
|
309
|
+
throw primaryError;
|
|
310
|
+
}
|
|
311
|
+
const fallbackModel = advisorFallbackModelRef;
|
|
312
|
+
if (fallbackModel === primaryModel) {
|
|
313
|
+
throw combinedFallbackError(
|
|
314
|
+
primaryModel,
|
|
315
|
+
primaryError,
|
|
316
|
+
fallbackModel,
|
|
317
|
+
new Error("fallback is the same model as the primary Advisor")
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
if (sameModelAdvisorDisabled(ctx, ctx.model, fallbackModel)) {
|
|
321
|
+
throw combinedFallbackError(
|
|
322
|
+
primaryModel,
|
|
323
|
+
primaryError,
|
|
324
|
+
fallbackModel,
|
|
325
|
+
new Error(fallbackSameModelAdvisorNotice)
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
const fallbackAccessReason = advisorModelAccessReason(ctx);
|
|
329
|
+
if (fallbackAccessReason) {
|
|
330
|
+
throw combinedFallbackError(
|
|
331
|
+
primaryModel,
|
|
332
|
+
primaryError,
|
|
333
|
+
fallbackModel,
|
|
334
|
+
new Error(fallbackAccessReason)
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
let resolvedFallback: Awaited<ReturnType<typeof resolveConfiguredModel>>;
|
|
338
|
+
try {
|
|
339
|
+
resolvedFallback = await resolveConfiguredModel(
|
|
340
|
+
ctx,
|
|
341
|
+
fallbackModel,
|
|
342
|
+
"Advisor fallback"
|
|
343
|
+
);
|
|
344
|
+
} catch (error) {
|
|
345
|
+
throw combinedFallbackError(
|
|
346
|
+
primaryModel,
|
|
347
|
+
primaryError,
|
|
348
|
+
fallbackModel,
|
|
349
|
+
error instanceof Error ? error : new Error(String(error))
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
if (!request) {
|
|
353
|
+
try {
|
|
354
|
+
request = options.followUp
|
|
355
|
+
? prepareFollowUp(options.followUp, options.question, options.ctx)
|
|
356
|
+
: await prepareFreshRequest(options, resolvedFallback);
|
|
357
|
+
} catch (error) {
|
|
358
|
+
throw combinedFallbackError(
|
|
359
|
+
primaryModel,
|
|
360
|
+
primaryError,
|
|
361
|
+
fallbackModel,
|
|
362
|
+
error instanceof Error ? error : new Error(String(error))
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
try {
|
|
367
|
+
return await runAdvisorAttempt(
|
|
368
|
+
options,
|
|
369
|
+
request,
|
|
370
|
+
resolvedFallback,
|
|
371
|
+
fallbackModel
|
|
372
|
+
);
|
|
373
|
+
} catch (error) {
|
|
374
|
+
if (options.signal?.aborted) {
|
|
375
|
+
throw error;
|
|
376
|
+
}
|
|
377
|
+
throw combinedFallbackError(
|
|
378
|
+
primaryModel,
|
|
379
|
+
primaryError,
|
|
380
|
+
fallbackModel,
|
|
381
|
+
error instanceof Error ? error : new Error(String(error))
|
|
382
|
+
);
|
|
179
383
|
}
|
|
180
|
-
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
const collectAdvisorResponse = async (
|
|
387
|
+
options: CollectAdvisorResponseOptions
|
|
388
|
+
): Promise<CollectedAdvisorResponse> => {
|
|
389
|
+
const { ctx, signal } = options;
|
|
390
|
+
loadConfig(ctx);
|
|
391
|
+
const primaryModel = options.followUp?.payload.model ?? advisorRef;
|
|
392
|
+
if (sameModelAdvisorDisabled(ctx, ctx.model, primaryModel)) {
|
|
393
|
+
throw new Error(
|
|
394
|
+
primaryModel === advisorRef
|
|
395
|
+
? sameModelAdvisorNotice
|
|
396
|
+
: fallbackSameModelAdvisorNotice
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
const accessReason = advisorModelAccessReason(ctx);
|
|
400
|
+
if (accessReason) {
|
|
401
|
+
throw new Error(accessReason);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
let primaryError: Error | undefined;
|
|
405
|
+
let prepared: PreparedAdvisorRequest | undefined;
|
|
406
|
+
let resolvedPrimary:
|
|
407
|
+
| Awaited<ReturnType<typeof resolveConfiguredModel>>
|
|
408
|
+
| undefined;
|
|
409
|
+
try {
|
|
410
|
+
resolvedPrimary = await resolveConfiguredModel(
|
|
411
|
+
ctx,
|
|
412
|
+
primaryModel,
|
|
413
|
+
"Advisor"
|
|
414
|
+
);
|
|
415
|
+
} catch (error) {
|
|
416
|
+
primaryError = error instanceof Error ? error : new Error(String(error));
|
|
417
|
+
}
|
|
418
|
+
if (resolvedPrimary) {
|
|
419
|
+
prepared = options.followUp
|
|
420
|
+
? prepareFollowUp(options.followUp, options.question, options.ctx)
|
|
421
|
+
: await prepareFreshRequest(options, resolvedPrimary);
|
|
422
|
+
try {
|
|
423
|
+
return await runAdvisorAttempt(
|
|
424
|
+
options,
|
|
425
|
+
prepared,
|
|
426
|
+
resolvedPrimary,
|
|
427
|
+
primaryModel
|
|
428
|
+
);
|
|
429
|
+
} catch (error) {
|
|
430
|
+
primaryError = error instanceof Error ? error : new Error(String(error));
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
if (!primaryError) {
|
|
434
|
+
throw new Error("Advisor request failed before an error was recorded.");
|
|
435
|
+
}
|
|
436
|
+
if (signal?.aborted) {
|
|
437
|
+
throw primaryError;
|
|
438
|
+
}
|
|
439
|
+
return runFallbackAdvisorAttempt(
|
|
440
|
+
options,
|
|
441
|
+
primaryModel,
|
|
442
|
+
primaryError,
|
|
443
|
+
prepared
|
|
444
|
+
);
|
|
181
445
|
};
|
|
182
446
|
|
|
183
447
|
export const consultAdvisor = async (
|
|
@@ -191,12 +455,15 @@ export const consultAdvisor = async (
|
|
|
191
455
|
includeUntracked?: string[],
|
|
192
456
|
includeTracked?: string[],
|
|
193
457
|
onScout?: (event: ScoutLifecycleEvent) => void,
|
|
194
|
-
currentInvocationId?: string
|
|
458
|
+
currentInvocationId?: string,
|
|
459
|
+
followUp?: AdvisorFollowUpRequest,
|
|
460
|
+
onFollowUpPayload?: AdvisorFollowUpCapture
|
|
195
461
|
): Promise<AdvisorConsultationResult> => {
|
|
196
462
|
const result = await collectAdvisorResponse({
|
|
197
463
|
ctx,
|
|
198
464
|
currentInvocationId,
|
|
199
465
|
draft,
|
|
466
|
+
followUp,
|
|
200
467
|
gitContext,
|
|
201
468
|
includeTracked,
|
|
202
469
|
includeUntracked,
|
|
@@ -206,7 +473,10 @@ export const consultAdvisor = async (
|
|
|
206
473
|
signal,
|
|
207
474
|
systemPrompt: ADVISOR_SYSTEM,
|
|
208
475
|
});
|
|
209
|
-
|
|
476
|
+
const adviceId = randomUUID();
|
|
477
|
+
const { followUpPayload, ...publicResult } = result;
|
|
478
|
+
onFollowUpPayload?.(adviceId, followUpPayload, followUp?.adviceId);
|
|
479
|
+
return { ...publicResult, adviceId, trigger };
|
|
210
480
|
};
|
|
211
481
|
|
|
212
482
|
export const runAdvisorGate = async (
|
|
@@ -41,17 +41,21 @@ export const advisorModelAccess = (
|
|
|
41
41
|
|
|
42
42
|
export const sameModelAdvisorDisabled = (
|
|
43
43
|
ctx: Pick<ExtensionContext, "model">,
|
|
44
|
-
model: { id: string; provider: string } | undefined = ctx.model
|
|
44
|
+
model: { id: string; provider: string } | undefined = ctx.model,
|
|
45
|
+
advisorModelRef: string | undefined = advisorRef
|
|
45
46
|
): boolean => {
|
|
46
|
-
|
|
47
|
+
const selectedAdvisorRef = advisorModelRef ?? advisorRef;
|
|
48
|
+
if (!(advisorDisableSameModelRef && selectedAdvisorRef && model)) {
|
|
47
49
|
return false;
|
|
48
50
|
}
|
|
49
|
-
const [provider, id] = splitRef(
|
|
51
|
+
const [provider, id] = splitRef(selectedAdvisorRef);
|
|
50
52
|
return provider === model.provider && id === model.id;
|
|
51
53
|
};
|
|
52
54
|
|
|
53
55
|
export const sameModelAdvisorNotice =
|
|
54
56
|
"Advisor disabled: executor and advisor are the same model.";
|
|
57
|
+
export const fallbackSameModelAdvisorNotice =
|
|
58
|
+
"Advisor fallback skipped: executor and fallback Advisor are the same model.";
|
|
55
59
|
|
|
56
60
|
export const advisorModelIsAllowed = (
|
|
57
61
|
ctx: Pick<ExtensionContext, "model">
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
isSimpleMode,
|
|
12
12
|
} from "../config/state.ts";
|
|
13
13
|
import { loadConfig } from "../config/storage.ts";
|
|
14
|
+
import type { AdvisorFollowUpPayload } from "../follow-up.ts";
|
|
14
15
|
import { notifyHerdrAdvisorFailure } from "../herdr.ts";
|
|
15
16
|
import {
|
|
16
17
|
ADVISOR_STREAM_UPDATE_INTERVAL_MS,
|
|
@@ -21,10 +22,12 @@ import {
|
|
|
21
22
|
advisorUsageForPi,
|
|
22
23
|
snapshotAdvisorUsage,
|
|
23
24
|
} from "../usage.ts";
|
|
25
|
+
import { assertAdvisorFollowUpPrivacy } from "./consultation.ts";
|
|
24
26
|
import { notifyLocalFailure, updateAdvisorUsageStatus } from "./gate-policy.ts";
|
|
25
27
|
import { normalizeScreeningQuestion, screeningSkipText } from "./jev-filter.ts";
|
|
26
28
|
import {
|
|
27
29
|
advisorModelAccessReason,
|
|
30
|
+
fallbackSameModelAdvisorNotice,
|
|
28
31
|
sameModelAdvisorDisabled,
|
|
29
32
|
sameModelAdvisorNotice,
|
|
30
33
|
} from "./model-access.ts";
|
|
@@ -47,6 +50,65 @@ const assertAdvisorModelAccess = (ctx: ExtensionContext) => {
|
|
|
47
50
|
}
|
|
48
51
|
};
|
|
49
52
|
|
|
53
|
+
interface AskAdvisorParams {
|
|
54
|
+
draft?: string;
|
|
55
|
+
followUpTo?: string;
|
|
56
|
+
force?: boolean;
|
|
57
|
+
gitContext?: "none" | "summary" | "full";
|
|
58
|
+
includeTrackedFiles?: string[];
|
|
59
|
+
includeUntracked?: string[];
|
|
60
|
+
question?: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const resolveFollowUp = (
|
|
64
|
+
params: AskAdvisorParams,
|
|
65
|
+
session: ToolRegistrationContext["session"]
|
|
66
|
+
): { adviceId: string; payload: AdvisorFollowUpPayload } | undefined => {
|
|
67
|
+
if (!params.followUpTo) {
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
if (
|
|
71
|
+
!params.question?.trim() ||
|
|
72
|
+
params.draft !== undefined ||
|
|
73
|
+
params.gitContext !== undefined ||
|
|
74
|
+
params.includeTrackedFiles?.length ||
|
|
75
|
+
params.includeUntracked?.length
|
|
76
|
+
) {
|
|
77
|
+
throw new Error(
|
|
78
|
+
"Follow-up consultations accept only followUpTo and a non-empty question. Issue a fresh consultation for new context or attachments."
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
const cached = session.followUpFor(params.followUpTo);
|
|
82
|
+
if (!cached.payload) {
|
|
83
|
+
throw new Error(
|
|
84
|
+
`Follow-up unavailable for adviceId ${params.followUpTo}: ${cached.reason ?? "the cached payload is unavailable"}. Issue a fresh consultation.`
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
return { adviceId: params.followUpTo, payload: cached.payload };
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const sameModelNoticeFor = (modelRef: string | undefined) =>
|
|
91
|
+
(modelRef ?? advisorRef) === advisorRef
|
|
92
|
+
? sameModelAdvisorNotice
|
|
93
|
+
: fallbackSameModelAdvisorNotice;
|
|
94
|
+
|
|
95
|
+
const skippedSameModelResult = (
|
|
96
|
+
id: string,
|
|
97
|
+
notice: string,
|
|
98
|
+
reservedCalls: Set<string>,
|
|
99
|
+
session: ToolRegistrationContext["session"]
|
|
100
|
+
) => {
|
|
101
|
+
session.releaseCall(id);
|
|
102
|
+
reservedCalls.delete(id);
|
|
103
|
+
return {
|
|
104
|
+
content: [{ text: notice, type: "text" as const }],
|
|
105
|
+
details: {
|
|
106
|
+
skipReason: notice,
|
|
107
|
+
text: notice,
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
};
|
|
111
|
+
|
|
50
112
|
const claimTrackedHandoff = (
|
|
51
113
|
session: ToolRegistrationContext["session"],
|
|
52
114
|
includeTrackedFiles: string[] | undefined
|
|
@@ -76,23 +138,25 @@ export const registerAskAdvisorTool = ({
|
|
|
76
138
|
}: ToolRegistrationContext): void => {
|
|
77
139
|
pi.registerTool({
|
|
78
140
|
description:
|
|
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
|
|
141
|
+
"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. Use followUpTo with a new question to continue one cached consultation without rebuilding its redacted context. If the Advisor explicitly names a missing file, you may make a sequential fresh consultation with includeTrackedFiles when enabled and relevant.",
|
|
80
142
|
async execute(_id, params, signal, onUpdate, ctx) {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
return {
|
|
85
|
-
content: [{ text: sameModelAdvisorNotice, type: "text" as const }],
|
|
86
|
-
details: {
|
|
87
|
-
skipReason: sameModelAdvisorNotice,
|
|
88
|
-
text: sameModelAdvisorNotice,
|
|
89
|
-
},
|
|
90
|
-
};
|
|
91
|
-
};
|
|
143
|
+
let followUp:
|
|
144
|
+
| { adviceId: string; payload: AdvisorFollowUpPayload }
|
|
145
|
+
| undefined;
|
|
92
146
|
try {
|
|
93
147
|
loadConfig(ctx);
|
|
94
|
-
|
|
95
|
-
|
|
148
|
+
followUp = resolveFollowUp(params, session);
|
|
149
|
+
if (followUp) {
|
|
150
|
+
assertAdvisorFollowUpPrivacy(followUp.payload, ctx);
|
|
151
|
+
}
|
|
152
|
+
const modelRef = followUp?.payload.model;
|
|
153
|
+
if (sameModelAdvisorDisabled(ctx, ctx.model, modelRef)) {
|
|
154
|
+
return skippedSameModelResult(
|
|
155
|
+
_id,
|
|
156
|
+
sameModelNoticeFor(modelRef),
|
|
157
|
+
reservedCalls,
|
|
158
|
+
session
|
|
159
|
+
);
|
|
96
160
|
}
|
|
97
161
|
assertAdvisorModelAccess(ctx);
|
|
98
162
|
} catch (error) {
|
|
@@ -148,8 +212,13 @@ export const registerAskAdvisorTool = ({
|
|
|
148
212
|
},
|
|
149
213
|
};
|
|
150
214
|
}
|
|
151
|
-
if (sameModelAdvisorDisabled(ctx)) {
|
|
152
|
-
return
|
|
215
|
+
if (sameModelAdvisorDisabled(ctx, ctx.model, followUp?.payload.model)) {
|
|
216
|
+
return skippedSameModelResult(
|
|
217
|
+
_id,
|
|
218
|
+
sameModelNoticeFor(followUp?.payload.model),
|
|
219
|
+
reservedCalls,
|
|
220
|
+
session
|
|
221
|
+
);
|
|
153
222
|
}
|
|
154
223
|
if (
|
|
155
224
|
!simpleMode &&
|
|
@@ -191,7 +260,8 @@ export const registerAskAdvisorTool = ({
|
|
|
191
260
|
coalescedUpdate.update({
|
|
192
261
|
content: [{ text: tx, type: "text" }],
|
|
193
262
|
details: {
|
|
194
|
-
advisor: advisorRef,
|
|
263
|
+
advisor: followUp?.payload.model ?? advisorRef,
|
|
264
|
+
followUp: Boolean(followUp),
|
|
195
265
|
question: resolveAdvisorRequest(params.question),
|
|
196
266
|
scout: scoutDetails,
|
|
197
267
|
text: tx,
|
|
@@ -209,13 +279,17 @@ export const registerAskAdvisorTool = ({
|
|
|
209
279
|
coalescedUpdate.update({
|
|
210
280
|
content: [{ text: scoutDetails.text ?? "", type: "text" }],
|
|
211
281
|
details: {
|
|
212
|
-
advisor: advisorRef,
|
|
282
|
+
advisor: followUp?.payload.model ?? advisorRef,
|
|
283
|
+
followUp: Boolean(followUp),
|
|
213
284
|
question: resolveAdvisorRequest(params.question),
|
|
214
285
|
scout: scoutDetails,
|
|
215
286
|
},
|
|
216
287
|
});
|
|
217
288
|
},
|
|
218
|
-
_id
|
|
289
|
+
_id,
|
|
290
|
+
followUp,
|
|
291
|
+
(adviceId, payload, replacesAdviceId) =>
|
|
292
|
+
session.captureFollowUp(adviceId, payload, replacesAdviceId)
|
|
219
293
|
);
|
|
220
294
|
flushUpdate();
|
|
221
295
|
session.issueAdvice(
|
|
@@ -228,6 +302,7 @@ export const registerAskAdvisorTool = ({
|
|
|
228
302
|
session.recordInvocation({
|
|
229
303
|
cost: advisorUsageCost(result.usage),
|
|
230
304
|
executionEffect: "continued",
|
|
305
|
+
followUp: result.followUp,
|
|
231
306
|
kind: "markdown",
|
|
232
307
|
model: result.model,
|
|
233
308
|
trigger: "executor-requested",
|
|
@@ -241,6 +316,7 @@ export const registerAskAdvisorTool = ({
|
|
|
241
316
|
advisor: result.model,
|
|
242
317
|
agentRulesBytes: result.agentRulesBytes,
|
|
243
318
|
draftBytes: result.draftBytes,
|
|
319
|
+
followUp: result.followUp,
|
|
244
320
|
imageBytes: result.imageBytes,
|
|
245
321
|
imageCount: result.imageCount,
|
|
246
322
|
imageOmissions: result.imageOmissions,
|
|
@@ -259,7 +335,7 @@ export const registerAskAdvisorTool = ({
|
|
|
259
335
|
const response: AgentToolResult<AdvisorToolDetails> = {
|
|
260
336
|
content: [
|
|
261
337
|
{
|
|
262
|
-
text:
|
|
338
|
+
text: `${result.followUp ? "Advisor follow-up" : "Advisor"} (${result.model})\n\n${result.markdown}`,
|
|
263
339
|
type: "text",
|
|
264
340
|
},
|
|
265
341
|
],
|
|
@@ -280,7 +356,7 @@ export const registerAskAdvisorTool = ({
|
|
|
280
356
|
executionEffect: "continued",
|
|
281
357
|
failure: "provider-error",
|
|
282
358
|
kind: "markdown",
|
|
283
|
-
model: advisorRef,
|
|
359
|
+
model: followUp?.payload.model ?? advisorRef,
|
|
284
360
|
trigger: "executor-requested",
|
|
285
361
|
});
|
|
286
362
|
updateAdvisorUsageStatus(ctx, session);
|
|
@@ -303,6 +379,12 @@ export const registerAskAdvisorTool = ({
|
|
|
303
379
|
"Concise untrusted draft for plan or completion review; claims are not verification evidence.",
|
|
304
380
|
})
|
|
305
381
|
),
|
|
382
|
+
followUpTo: Type.Optional(
|
|
383
|
+
Type.String({
|
|
384
|
+
description:
|
|
385
|
+
"Opaque adviceId from a prior successful consultation. Requires a new non-empty question and reuses that consultation's redacted payload; do not combine with draft, Git context, or file attachments.",
|
|
386
|
+
})
|
|
387
|
+
),
|
|
306
388
|
force: Type.Optional(
|
|
307
389
|
Type.Boolean({
|
|
308
390
|
description:
|
|
@@ -342,12 +424,16 @@ export const registerAskAdvisorTool = ({
|
|
|
342
424
|
),
|
|
343
425
|
}),
|
|
344
426
|
promptGuidelines: [
|
|
345
|
-
"Call ask_advisor with an empty object for general consultation. For a plan or completion review, include a concise draft naming work, validation, and remaining risks; its claims are not evidence. If the Advisor explicitly says it cannot review a specifically named file, you may make a sequential follow-up call with includeTrackedFiles when the file is relevant, permitted, and worth the shared call budget; do not infer paths or retry automatically.",
|
|
427
|
+
"Call ask_advisor with an empty object for general consultation. For a plan or completion review, include a concise draft naming work, validation, and remaining risks; its claims are not evidence. To drill into an existing answer, pass its adviceId as followUpTo with a new question and no draft, Git context, or attachments. If the Advisor explicitly says it cannot review a specifically named file, you may make a sequential follow-up call with includeTrackedFiles when the file is relevant, permitted, and worth the shared call budget; do not infer paths or retry automatically.",
|
|
346
428
|
],
|
|
347
429
|
promptSnippet:
|
|
348
430
|
"Consult the Advisor using its existing context; attach a draft for plan or completion review",
|
|
349
431
|
renderCall(args, theme) {
|
|
350
|
-
return renderAdvisorCallBox(
|
|
432
|
+
return renderAdvisorCallBox(
|
|
433
|
+
args.question?.trim(),
|
|
434
|
+
theme,
|
|
435
|
+
Boolean(args.followUpTo)
|
|
436
|
+
);
|
|
351
437
|
},
|
|
352
438
|
renderResult(result, options, theme, context) {
|
|
353
439
|
// SAFETY: this tool's execute() only returns AdvisorToolDetails-shaped details.
|