pi-advisor-flow 0.5.5 → 0.6.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 +28 -1
- package/README.md +1 -1
- package/dist/index.js +5786 -0
- package/extensions/index.ts +9 -9
- package/package.json +16 -10
- 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 +8 -8
- 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 +4 -4
- package/src/commands/types.ts +5 -13
- package/src/commands.ts +2 -2
- package/src/config/args.ts +3 -3
- package/src/config/defaults.ts +3 -3
- package/src/config/schema.ts +322 -0
- package/src/config/state.ts +2 -2
- package/src/config/storage.ts +113 -103
- package/src/config/types.ts +1 -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/model-stream.ts +5 -8
- package/src/outcomes.ts +2 -5
- 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 +119 -76
- 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/loop-gate.ts +70 -61
- package/src/tools/prompts.ts +4 -7
- package/src/tools/register-ask-advisor.ts +9 -14
- package/src/tools/register-lifecycle.ts +7 -7
- package/src/tools/register-outcome.ts +5 -5
- package/src/tools/register-renderers.ts +5 -5
- package/src/tools/registration.ts +10 -10
- package/src/tools/render-advisor-result.ts +11 -8
- 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 +7 -7
- package/src/tools.ts +9 -9
- package/src/ui/manual-dialog-render.ts +10 -3
- package/src/ui/manual-dialog.ts +4 -9
- package/src/ui/model-selector.ts +7 -6
- package/src/ui/settings-formatting.ts +2 -2
- package/src/ui/settings-items.ts +4 -4
- package/src/ui/settings-mutations.ts +2 -2
- package/src/ui/settings-selector.ts +5 -5
- package/src/ui/text-setting-submenu.ts +6 -1
- package/src/ui/types.ts +2 -2
- package/src/ui.ts +4 -4
- package/src/usage.ts +1 -6
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_ADVISOR_TOOL_RESULT_MAX_BYTES,
|
|
3
|
+
DEFAULT_ADVISOR_TOOL_RESULT_MAX_LINES,
|
|
4
|
+
} from "./config/types.ts";
|
|
5
|
+
import { byteLength, capUtf8Bytes } from "./content-utils.ts";
|
|
6
|
+
|
|
7
|
+
export interface ToolResultTruncation {
|
|
8
|
+
content: string;
|
|
9
|
+
omittedLines: number;
|
|
10
|
+
totalBytes: number;
|
|
11
|
+
totalLines: number;
|
|
12
|
+
truncated: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const OMITTED_MARKER = "[... omitted tool-result section ...]";
|
|
16
|
+
|
|
17
|
+
export const capToolResult = (
|
|
18
|
+
value: string,
|
|
19
|
+
maxLines = DEFAULT_ADVISOR_TOOL_RESULT_MAX_LINES,
|
|
20
|
+
maxBytes = DEFAULT_ADVISOR_TOOL_RESULT_MAX_BYTES
|
|
21
|
+
): ToolResultTruncation => {
|
|
22
|
+
const lines = value.split("\n");
|
|
23
|
+
const totalLines = lines.length;
|
|
24
|
+
const totalBytes = byteLength(value);
|
|
25
|
+
if ((maxLines === 0 || maxBytes === 0) && value.length > 0) {
|
|
26
|
+
return {
|
|
27
|
+
content: "[Tool result omitted: configured limit is zero]",
|
|
28
|
+
omittedLines: totalLines,
|
|
29
|
+
totalBytes,
|
|
30
|
+
totalLines,
|
|
31
|
+
truncated: true,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
if (totalLines <= maxLines && totalBytes <= maxBytes) {
|
|
35
|
+
return {
|
|
36
|
+
content: value,
|
|
37
|
+
omittedLines: 0,
|
|
38
|
+
totalBytes,
|
|
39
|
+
totalLines,
|
|
40
|
+
truncated: false,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const markerBytes = byteLength(OMITTED_MARKER);
|
|
45
|
+
if (maxBytes < markerBytes || maxLines === 1) {
|
|
46
|
+
return {
|
|
47
|
+
content: capUtf8Bytes(OMITTED_MARKER, maxBytes),
|
|
48
|
+
omittedLines: totalLines,
|
|
49
|
+
totalBytes,
|
|
50
|
+
totalLines,
|
|
51
|
+
truncated: true,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
const headCount = Math.floor((maxLines - 1) / 2);
|
|
55
|
+
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
|
+
const availableBytes = maxBytes - markerBytes - 2;
|
|
74
|
+
const head = collect(lines, headCount, Math.floor(availableBytes / 2));
|
|
75
|
+
const tail = collect(
|
|
76
|
+
lines.slice(Math.max(head.length, lines.length - tailCount)),
|
|
77
|
+
tailCount,
|
|
78
|
+
availableBytes - byteLength(head.join("\n"))
|
|
79
|
+
);
|
|
80
|
+
const content = [...head, OMITTED_MARKER, ...tail].join("\n");
|
|
81
|
+
return {
|
|
82
|
+
content,
|
|
83
|
+
omittedLines: Math.max(0, totalLines - head.length - tail.length),
|
|
84
|
+
totalBytes,
|
|
85
|
+
totalLines,
|
|
86
|
+
truncated: true,
|
|
87
|
+
};
|
|
88
|
+
};
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import {
|
|
3
|
+
readTrackedFiles,
|
|
4
|
+
readUntrackedFiles,
|
|
5
|
+
type UntrackedAttachment,
|
|
6
|
+
} from "../attachments.ts";
|
|
7
|
+
import {
|
|
8
|
+
advisorGitContextMaxCharsRef,
|
|
9
|
+
advisorGitContextRef,
|
|
10
|
+
advisorRedactSecretsRef,
|
|
11
|
+
advisorScoutEnabledRef,
|
|
12
|
+
advisorTrackedFileContentRef,
|
|
13
|
+
advisorUntrackedContentRef,
|
|
14
|
+
contextMaxCharsRef,
|
|
15
|
+
} from "../config/state.ts";
|
|
16
|
+
import {
|
|
17
|
+
clampGitContextLevel,
|
|
18
|
+
collectGitContext,
|
|
19
|
+
type GitContextLevel,
|
|
20
|
+
} from "../git.ts";
|
|
21
|
+
import { readProjectPreferences } from "../preferences.ts";
|
|
22
|
+
import { redactAndCapText, redactSecrets } from "../redaction.ts";
|
|
23
|
+
import {
|
|
24
|
+
runAdvisorScout,
|
|
25
|
+
type ScoutLifecycleEvent,
|
|
26
|
+
type ScoutOutcome,
|
|
27
|
+
} from "../scout.ts";
|
|
28
|
+
import { curateAdvisorConversation } from "../scout-curation.ts";
|
|
29
|
+
import {
|
|
30
|
+
advisorGitContextBudget,
|
|
31
|
+
advisorRepositoryContext,
|
|
32
|
+
advisorRequestConversation,
|
|
33
|
+
} from "./prompts.ts";
|
|
34
|
+
|
|
35
|
+
/** Maximum bytes for project preferences and the redacted draft. */
|
|
36
|
+
const ATTACHMENT_TEXT_MAX_BYTES = 8 * 1024;
|
|
37
|
+
/** Combined budget for untracked plus tracked file attachments. */
|
|
38
|
+
const ATTACHMENTS_TOTAL_MAX_BYTES = 24 * 1024;
|
|
39
|
+
|
|
40
|
+
export interface ConsultationContextOptions {
|
|
41
|
+
ctx: ExtensionContext;
|
|
42
|
+
currentInvocationId?: string;
|
|
43
|
+
draft?: string;
|
|
44
|
+
gitContext?: GitContextLevel;
|
|
45
|
+
includeTracked?: string[];
|
|
46
|
+
includeUntracked?: string[];
|
|
47
|
+
onScout?: (event: ScoutLifecycleEvent) => void;
|
|
48
|
+
signal?: AbortSignal;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface ConsultationContext {
|
|
52
|
+
/** Disclosed repository changes header. */
|
|
53
|
+
changeText: string;
|
|
54
|
+
/** Curated (or legacy) conversation body. */
|
|
55
|
+
conversation: string;
|
|
56
|
+
/** Redacted draft text, if a draft was supplied. */
|
|
57
|
+
draftText?: string;
|
|
58
|
+
/** Redacted project preferences, if present. */
|
|
59
|
+
preferences?: { bytes: number; text: string };
|
|
60
|
+
/** Non-cancelling Scout outcome, when Scout ran. */
|
|
61
|
+
scout?: Exclude<ScoutOutcome, { cancelled: true }>;
|
|
62
|
+
/** Redacted tracked-file attachments. */
|
|
63
|
+
tracked: UntrackedAttachment[];
|
|
64
|
+
/** Redacted untracked-file attachments. */
|
|
65
|
+
untracked: UntrackedAttachment[];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Assembles every context region the Advisor request embeds: repository
|
|
69
|
+
* changes (spending the shared budget first), the curated conversation,
|
|
70
|
+
* project preferences, and consented file attachments. */
|
|
71
|
+
export const assembleConsultationContext = async (
|
|
72
|
+
options: ConsultationContextOptions
|
|
73
|
+
): Promise<ConsultationContext> => {
|
|
74
|
+
const { ctx } = options;
|
|
75
|
+
|
|
76
|
+
// The user setting is the ceiling; the Executor may only narrow it.
|
|
77
|
+
const allowed = advisorGitContextRef;
|
|
78
|
+
const level = clampGitContextLevel(options.gitContext ?? allowed, allowed);
|
|
79
|
+
const gitBudget = advisorGitContextBudget(
|
|
80
|
+
contextMaxCharsRef,
|
|
81
|
+
advisorGitContextMaxCharsRef
|
|
82
|
+
);
|
|
83
|
+
const changes = collectGitContext(
|
|
84
|
+
ctx.cwd,
|
|
85
|
+
level,
|
|
86
|
+
gitBudget,
|
|
87
|
+
advisorRedactSecretsRef ? redactSecrets : undefined
|
|
88
|
+
);
|
|
89
|
+
// The disclosure warning is control metadata, not repository payload. Keep it
|
|
90
|
+
// outside the zero-byte Git budget so disabling disclosure cannot erase it.
|
|
91
|
+
const changeText = advisorRepositoryContext(
|
|
92
|
+
changes,
|
|
93
|
+
options.gitContext ?? allowed,
|
|
94
|
+
level,
|
|
95
|
+
gitBudget
|
|
96
|
+
);
|
|
97
|
+
// Repository context spends part of the shared budget, so a large patch
|
|
98
|
+
// cannot silently push the conversation past the model's context window.
|
|
99
|
+
const conversationBudget = Math.max(
|
|
100
|
+
0,
|
|
101
|
+
contextMaxCharsRef - changeText.length
|
|
102
|
+
);
|
|
103
|
+
const legacyConversation = advisorRequestConversation(
|
|
104
|
+
ctx,
|
|
105
|
+
conversationBudget
|
|
106
|
+
);
|
|
107
|
+
const curated = await curateAdvisorConversation(
|
|
108
|
+
ctx,
|
|
109
|
+
legacyConversation,
|
|
110
|
+
options.signal,
|
|
111
|
+
options.onScout,
|
|
112
|
+
advisorScoutEnabledRef,
|
|
113
|
+
runAdvisorScout,
|
|
114
|
+
options.currentInvocationId,
|
|
115
|
+
conversationBudget
|
|
116
|
+
);
|
|
117
|
+
const preferences = await readProjectPreferences(
|
|
118
|
+
ctx,
|
|
119
|
+
ATTACHMENT_TEXT_MAX_BYTES,
|
|
120
|
+
advisorRedactSecretsRef
|
|
121
|
+
);
|
|
122
|
+
const draftText = options.draft
|
|
123
|
+
? redactAndCapText(
|
|
124
|
+
options.draft,
|
|
125
|
+
ATTACHMENT_TEXT_MAX_BYTES,
|
|
126
|
+
advisorRedactSecretsRef
|
|
127
|
+
)
|
|
128
|
+
: undefined;
|
|
129
|
+
const untracked = await readUntrackedFiles(
|
|
130
|
+
ctx.cwd,
|
|
131
|
+
options.includeUntracked ?? [],
|
|
132
|
+
advisorUntrackedContentRef,
|
|
133
|
+
advisorRedactSecretsRef
|
|
134
|
+
);
|
|
135
|
+
const tracked = await readTrackedFiles(
|
|
136
|
+
ctx.cwd,
|
|
137
|
+
options.includeTracked ?? [],
|
|
138
|
+
advisorTrackedFileContentRef,
|
|
139
|
+
advisorRedactSecretsRef,
|
|
140
|
+
Math.max(
|
|
141
|
+
0,
|
|
142
|
+
ATTACHMENTS_TOTAL_MAX_BYTES -
|
|
143
|
+
untracked.reduce((sum, item) => sum + item.bytes, 0)
|
|
144
|
+
)
|
|
145
|
+
);
|
|
146
|
+
return {
|
|
147
|
+
changeText,
|
|
148
|
+
conversation: curated.conversation,
|
|
149
|
+
draftText,
|
|
150
|
+
preferences,
|
|
151
|
+
scout: curated.scout,
|
|
152
|
+
tracked,
|
|
153
|
+
untracked,
|
|
154
|
+
};
|
|
155
|
+
};
|
|
@@ -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,})(.*)$/;
|