pi-advisor-flow 0.2.2 → 0.2.4
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/README.md +116 -67
- package/package.json +1 -1
- package/src/commands.ts +111 -30
- package/src/config.ts +122 -4
- package/src/conversation.ts +102 -19
- package/src/tools.ts +71 -15
- package/src/ui.ts +374 -80
package/src/config.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
1
|
+
import { existsSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import {
|
|
4
4
|
CONFIG_DIR_NAME,
|
|
@@ -14,6 +14,13 @@ export const DEFAULT_CONTEXT_MAX_CHARS = 15_000;
|
|
|
14
14
|
export const MAX_CONTEXT_MAX_CHARS = Number.MAX_SAFE_INTEGER;
|
|
15
15
|
export const DEFAULT_ADVISOR_TOOL_RESULT_MAX_LINES = DEFAULT_MAX_LINES;
|
|
16
16
|
export const DEFAULT_ADVISOR_TOOL_RESULT_MAX_BYTES = DEFAULT_MAX_BYTES;
|
|
17
|
+
export type AdvisorToolPolicy = "full" | "summary" | "exclude";
|
|
18
|
+
export type AdvisorToolPolicies = Record<string, AdvisorToolPolicy>;
|
|
19
|
+
export const ADVISOR_TOOL_POLICIES: AdvisorToolPolicy[] = [
|
|
20
|
+
"full",
|
|
21
|
+
"summary",
|
|
22
|
+
"exclude",
|
|
23
|
+
];
|
|
17
24
|
export type GateFailureMode =
|
|
18
25
|
| "block-session"
|
|
19
26
|
| "block-tool"
|
|
@@ -38,11 +45,15 @@ export let advisorBlockOnBlockedRef = true;
|
|
|
38
45
|
export let advisorAutoLoopGateRef = true;
|
|
39
46
|
export let advisorLoopThresholdRef = 3;
|
|
40
47
|
export let advisorMaxCallsPerSessionRef: number | undefined;
|
|
41
|
-
export let advisorSessionSummaryRef =
|
|
48
|
+
export let advisorSessionSummaryRef = false;
|
|
49
|
+
export let simpleModeRef = false;
|
|
50
|
+
export let alwaysOnRef = false;
|
|
42
51
|
export let advisorFailureModeRef: GateFailureMode = "block-session";
|
|
43
52
|
export let advisorHerdrIntegrationRef = true;
|
|
44
53
|
export let advisorToolResultMaxLinesRef = DEFAULT_ADVISOR_TOOL_RESULT_MAX_LINES;
|
|
45
54
|
export let advisorToolResultMaxBytesRef = DEFAULT_ADVISOR_TOOL_RESULT_MAX_BYTES;
|
|
55
|
+
export let advisorRedactSecretsRef = false;
|
|
56
|
+
export let advisorToolPoliciesRef: AdvisorToolPolicies = {};
|
|
46
57
|
|
|
47
58
|
export const setExecutorRef = (ref: string) => {
|
|
48
59
|
executorRef = ref;
|
|
@@ -98,6 +109,14 @@ export const setAdvisorMaxCallsPerSessionRef = (value: number | undefined) => {
|
|
|
98
109
|
export const setAdvisorSessionSummaryRef = (enabled: boolean) => {
|
|
99
110
|
advisorSessionSummaryRef = enabled;
|
|
100
111
|
};
|
|
112
|
+
export const setSimpleModeRef = (enabled: boolean) => {
|
|
113
|
+
simpleModeRef = enabled;
|
|
114
|
+
};
|
|
115
|
+
export const setAlwaysOnRef = (enabled: boolean) => {
|
|
116
|
+
alwaysOnRef = enabled;
|
|
117
|
+
};
|
|
118
|
+
/** Whether advanced Advisor automation is suppressed for this session. */
|
|
119
|
+
export const isSimpleMode = () => simpleModeRef;
|
|
101
120
|
export const isValidGateFailureMode = (
|
|
102
121
|
value: unknown
|
|
103
122
|
): value is GateFailureMode =>
|
|
@@ -119,12 +138,19 @@ export const setAdvisorToolResultMaxLinesRef = (value: number) => {
|
|
|
119
138
|
export const setAdvisorToolResultMaxBytesRef = (value: number) => {
|
|
120
139
|
advisorToolResultMaxBytesRef = value;
|
|
121
140
|
};
|
|
141
|
+
export const setAdvisorRedactSecretsRef = (enabled: boolean) => {
|
|
142
|
+
advisorRedactSecretsRef = enabled;
|
|
143
|
+
};
|
|
144
|
+
export const setAdvisorToolPoliciesRef = (policies: AdvisorToolPolicies) => {
|
|
145
|
+
advisorToolPoliciesRef = { ...policies };
|
|
146
|
+
};
|
|
122
147
|
|
|
123
148
|
/**
|
|
124
149
|
* Returns the current live settings state. Use this at UI boundaries instead of
|
|
125
150
|
* imported mutable bindings, which can be snapshotted by extension loaders.
|
|
126
151
|
*/
|
|
127
152
|
export const getAdvisorSettings = () => ({
|
|
153
|
+
alwaysOn: alwaysOnRef,
|
|
128
154
|
autoLoopGate: advisorAutoLoopGateRef,
|
|
129
155
|
blockOnBlocked: advisorBlockOnBlockedRef,
|
|
130
156
|
collapseResponses: advisorCollapseResponsesRef,
|
|
@@ -138,7 +164,10 @@ export const getAdvisorSettings = () => ({
|
|
|
138
164
|
loopThreshold: advisorLoopThresholdRef,
|
|
139
165
|
maxCallsPerSession: advisorMaxCallsPerSessionRef,
|
|
140
166
|
planGate: advisorPlanGateRef,
|
|
167
|
+
redactSecrets: advisorRedactSecretsRef,
|
|
141
168
|
sessionSummary: advisorSessionSummaryRef,
|
|
169
|
+
simpleMode: simpleModeRef,
|
|
170
|
+
toolPolicies: { ...advisorToolPoliciesRef },
|
|
142
171
|
toolResultMaxBytes: advisorToolResultMaxBytesRef,
|
|
143
172
|
toolResultMaxLines: advisorToolResultMaxLinesRef,
|
|
144
173
|
});
|
|
@@ -168,13 +197,17 @@ export interface AdvisorConfig {
|
|
|
168
197
|
advisorLoopThreshold?: number;
|
|
169
198
|
advisorMaxCallsPerSession?: number;
|
|
170
199
|
advisorPlanGate?: boolean;
|
|
200
|
+
advisorRedactSecrets?: boolean;
|
|
171
201
|
advisorSessionSummary?: boolean;
|
|
202
|
+
advisorToolPolicies?: AdvisorToolPolicies;
|
|
172
203
|
advisorToolResultMaxBytes?: number;
|
|
173
204
|
advisorToolResultMaxLines?: number;
|
|
205
|
+
alwaysOn?: boolean;
|
|
174
206
|
contextMaxChars?: number;
|
|
175
207
|
executor?: string;
|
|
176
208
|
executorEffort?: string;
|
|
177
209
|
gateFailureMode?: GateFailureMode;
|
|
210
|
+
simpleMode?: boolean;
|
|
178
211
|
}
|
|
179
212
|
|
|
180
213
|
const CONFIG_KEYS = new Set<keyof AdvisorConfig>([
|
|
@@ -191,8 +224,12 @@ const CONFIG_KEYS = new Set<keyof AdvisorConfig>([
|
|
|
191
224
|
"advisorMaxCallsPerSession",
|
|
192
225
|
"advisorPlanGate",
|
|
193
226
|
"advisorSessionSummary",
|
|
227
|
+
"simpleMode",
|
|
228
|
+
"alwaysOn",
|
|
194
229
|
"advisorToolResultMaxBytes",
|
|
195
230
|
"advisorToolResultMaxLines",
|
|
231
|
+
"advisorRedactSecrets",
|
|
232
|
+
"advisorToolPolicies",
|
|
196
233
|
"contextMaxChars",
|
|
197
234
|
"executor",
|
|
198
235
|
"executorEffort",
|
|
@@ -206,7 +243,10 @@ const BOOLEAN_CONFIG_KEYS = [
|
|
|
206
243
|
"advisorBlockOnBlocked",
|
|
207
244
|
"advisorAutoLoopGate",
|
|
208
245
|
"advisorSessionSummary",
|
|
246
|
+
"simpleMode",
|
|
247
|
+
"alwaysOn",
|
|
209
248
|
"advisorHerdrIntegration",
|
|
249
|
+
"advisorRedactSecrets",
|
|
210
250
|
] as const;
|
|
211
251
|
const STRING_CONFIG_KEYS = [
|
|
212
252
|
"executor",
|
|
@@ -262,6 +302,20 @@ const validateBooleanValues = (config: ConfigRecord, path: string) => {
|
|
|
262
302
|
}
|
|
263
303
|
};
|
|
264
304
|
|
|
305
|
+
export const isValidAdvisorToolPolicies = (
|
|
306
|
+
value: unknown
|
|
307
|
+
): value is AdvisorToolPolicies => {
|
|
308
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
309
|
+
return false;
|
|
310
|
+
}
|
|
311
|
+
return Object.entries(value).every(
|
|
312
|
+
([toolName, policy]) =>
|
|
313
|
+
toolName.trim().length > 0 &&
|
|
314
|
+
typeof policy === "string" &&
|
|
315
|
+
ADVISOR_TOOL_POLICIES.includes(policy as AdvisorToolPolicy)
|
|
316
|
+
);
|
|
317
|
+
};
|
|
318
|
+
|
|
265
319
|
const validateNumericValues = (config: ConfigRecord, path: string) => {
|
|
266
320
|
const numericRules: [
|
|
267
321
|
keyof AdvisorConfig,
|
|
@@ -315,6 +369,16 @@ export const validateConfig = (
|
|
|
315
369
|
validateStringValues(config, path);
|
|
316
370
|
validateBooleanValues(config, path);
|
|
317
371
|
validateNumericValues(config, path);
|
|
372
|
+
if (
|
|
373
|
+
config.advisorToolPolicies !== undefined &&
|
|
374
|
+
!isValidAdvisorToolPolicies(config.advisorToolPolicies)
|
|
375
|
+
) {
|
|
376
|
+
invalidConfigValue(
|
|
377
|
+
path,
|
|
378
|
+
"advisorToolPolicies",
|
|
379
|
+
"a JSON object with non-empty tool names and full, summary, or exclude values"
|
|
380
|
+
);
|
|
381
|
+
}
|
|
318
382
|
if (
|
|
319
383
|
config.gateFailureMode !== undefined &&
|
|
320
384
|
!isValidGateFailureMode(config.gateFailureMode)
|
|
@@ -339,11 +403,15 @@ const resetDefaults = () => {
|
|
|
339
403
|
advisorAutoLoopGateRef = true;
|
|
340
404
|
advisorLoopThresholdRef = 3;
|
|
341
405
|
advisorMaxCallsPerSessionRef = undefined;
|
|
342
|
-
advisorSessionSummaryRef =
|
|
406
|
+
advisorSessionSummaryRef = false;
|
|
407
|
+
simpleModeRef = false;
|
|
408
|
+
alwaysOnRef = false;
|
|
343
409
|
advisorFailureModeRef = "block-session";
|
|
344
410
|
advisorHerdrIntegrationRef = true;
|
|
345
411
|
advisorToolResultMaxLinesRef = DEFAULT_ADVISOR_TOOL_RESULT_MAX_LINES;
|
|
346
412
|
advisorToolResultMaxBytesRef = DEFAULT_ADVISOR_TOOL_RESULT_MAX_BYTES;
|
|
413
|
+
advisorRedactSecretsRef = false;
|
|
414
|
+
advisorToolPoliciesRef = {};
|
|
347
415
|
};
|
|
348
416
|
|
|
349
417
|
const applyOptionalConfig = <Key extends keyof AdvisorConfig>(
|
|
@@ -410,6 +478,8 @@ const applyConfig = (config: AdvisorConfig) => {
|
|
|
410
478
|
"advisorSessionSummary",
|
|
411
479
|
setAdvisorSessionSummaryRef
|
|
412
480
|
);
|
|
481
|
+
applyOptionalConfig(config, "simpleMode", setSimpleModeRef);
|
|
482
|
+
applyOptionalConfig(config, "alwaysOn", setAlwaysOnRef);
|
|
413
483
|
applyOptionalConfig(config, "gateFailureMode", setAdvisorFailureModeRef);
|
|
414
484
|
applyOptionalConfig(
|
|
415
485
|
config,
|
|
@@ -426,6 +496,12 @@ const applyConfig = (config: AdvisorConfig) => {
|
|
|
426
496
|
"advisorToolResultMaxBytes",
|
|
427
497
|
setAdvisorToolResultMaxBytesRef
|
|
428
498
|
);
|
|
499
|
+
applyOptionalConfig(
|
|
500
|
+
config,
|
|
501
|
+
"advisorRedactSecrets",
|
|
502
|
+
setAdvisorRedactSecretsRef
|
|
503
|
+
);
|
|
504
|
+
applyOptionalConfig(config, "advisorToolPolicies", setAdvisorToolPoliciesRef);
|
|
429
505
|
};
|
|
430
506
|
|
|
431
507
|
const readConfig = (path: string): AdvisorConfig => {
|
|
@@ -438,6 +514,43 @@ const readConfig = (path: string): AdvisorConfig => {
|
|
|
438
514
|
}
|
|
439
515
|
};
|
|
440
516
|
|
|
517
|
+
// loadConfig runs on every tool call and every consultation. Caching the parsed
|
|
518
|
+
// file by path and stat identity removes that read and parse from the hot path
|
|
519
|
+
// while still applying the full reset-then-apply sequence on each call.
|
|
520
|
+
let configCache:
|
|
521
|
+
| { config: AdvisorConfig; identity: string; path: string }
|
|
522
|
+
| undefined;
|
|
523
|
+
|
|
524
|
+
/** Drops the parsed-configuration cache; the next load re-reads from disk. */
|
|
525
|
+
export const resetConfigCache = () => {
|
|
526
|
+
configCache = undefined;
|
|
527
|
+
};
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* Nanosecond timestamps plus size and inode so a same-millisecond or
|
|
531
|
+
* same-size external rewrite cannot be served from the cache. An unstattable
|
|
532
|
+
* path yields a unique value, which always forces a re-read.
|
|
533
|
+
*/
|
|
534
|
+
const configIdentity = (path: string): string => {
|
|
535
|
+
try {
|
|
536
|
+
const stats = statSync(path, { bigint: true });
|
|
537
|
+
return `${stats.mtimeNs}:${stats.ctimeNs}:${stats.size}:${stats.ino}:${stats.dev}`;
|
|
538
|
+
} catch {
|
|
539
|
+
return `unstattable:${process.hrtime.bigint()}`;
|
|
540
|
+
}
|
|
541
|
+
};
|
|
542
|
+
|
|
543
|
+
const readConfigCached = (path: string): AdvisorConfig => {
|
|
544
|
+
const identity = configIdentity(path);
|
|
545
|
+
const cached = configCache;
|
|
546
|
+
if (cached && cached.path === path && cached.identity === identity) {
|
|
547
|
+
return cached.config;
|
|
548
|
+
}
|
|
549
|
+
const config = readConfig(path);
|
|
550
|
+
configCache = { config, identity, path };
|
|
551
|
+
return config;
|
|
552
|
+
};
|
|
553
|
+
|
|
441
554
|
export const loadConfig = (ctx: ExtensionContext) => {
|
|
442
555
|
resetDefaults();
|
|
443
556
|
const path = configPaths(ctx).find(
|
|
@@ -447,7 +560,7 @@ export const loadConfig = (ctx: ExtensionContext) => {
|
|
|
447
560
|
if (!path) {
|
|
448
561
|
return null;
|
|
449
562
|
}
|
|
450
|
-
applyConfig(
|
|
563
|
+
applyConfig(readConfigCached(path));
|
|
451
564
|
return path;
|
|
452
565
|
};
|
|
453
566
|
|
|
@@ -488,12 +601,17 @@ export const saveConfig = (ctx: ExtensionContext) => {
|
|
|
488
601
|
? {}
|
|
489
602
|
: { advisorMaxCallsPerSession: advisorMaxCallsPerSessionRef }),
|
|
490
603
|
advisorHerdrIntegration: advisorHerdrIntegrationRef,
|
|
604
|
+
advisorRedactSecrets: advisorRedactSecretsRef,
|
|
491
605
|
advisorSessionSummary: advisorSessionSummaryRef,
|
|
606
|
+
advisorToolPolicies: advisorToolPoliciesRef,
|
|
492
607
|
advisorToolResultMaxBytes: advisorToolResultMaxBytesRef,
|
|
493
608
|
advisorToolResultMaxLines: advisorToolResultMaxLinesRef,
|
|
609
|
+
alwaysOn: alwaysOnRef,
|
|
494
610
|
gateFailureMode: advisorFailureModeRef,
|
|
611
|
+
simpleMode: simpleModeRef,
|
|
495
612
|
};
|
|
496
613
|
writeFileSync(path, `${JSON.stringify(data, null, 2)}\n`);
|
|
614
|
+
resetConfigCache();
|
|
497
615
|
return path;
|
|
498
616
|
};
|
|
499
617
|
|
package/src/conversation.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import {
|
|
3
|
+
type AdvisorToolPolicies,
|
|
4
|
+
advisorRedactSecretsRef,
|
|
5
|
+
advisorToolPoliciesRef,
|
|
3
6
|
advisorToolResultMaxBytesRef,
|
|
4
7
|
advisorToolResultMaxLinesRef,
|
|
5
8
|
DEFAULT_ADVISOR_TOOL_RESULT_MAX_BYTES,
|
|
@@ -33,6 +36,29 @@ export const textFrom = (content: unknown): string =>
|
|
|
33
36
|
|
|
34
37
|
const byteLength = (value: string) => Buffer.byteLength(value, "utf8");
|
|
35
38
|
|
|
39
|
+
const REDACTION_MARKER = "[REDACTED SECRET]";
|
|
40
|
+
const SECRET_PATTERNS = [
|
|
41
|
+
/-----BEGIN(?: [A-Z0-9]+)? PRIVATE KEY-----[\s\S]*?-----END(?: [A-Z0-9]+)? PRIVATE KEY-----/gi,
|
|
42
|
+
/\bBearer\s+[A-Za-z0-9._~+/=-]{8,}/gi,
|
|
43
|
+
/\b(?:api[_-]?key|token|secret|password|passwd)\s*[:=]\s*(?:"[^"]*"|'[^']*'|[^\s"'&,;)}\]]+)/gi,
|
|
44
|
+
/([a-z][a-z0-9+.-]*:\/\/)[^\s/@:]+:[^\s/@]+@/gi,
|
|
45
|
+
/\b(?:AKIA|ASIA)[A-Z0-9]{16}\b/g,
|
|
46
|
+
/\b(?:aws_secret_access_key|aws_session_token)\s*[:=]\s*[^\s"'&,;)}\]]+/gi,
|
|
47
|
+
] as const;
|
|
48
|
+
|
|
49
|
+
/** Redacts common credential forms locally; it is not a data-classification system. */
|
|
50
|
+
export const redactSecrets = (value: string): string => {
|
|
51
|
+
let redacted = value;
|
|
52
|
+
for (const pattern of SECRET_PATTERNS) {
|
|
53
|
+
redacted = redacted.replace(pattern, (_match, scheme) =>
|
|
54
|
+
typeof scheme === "string"
|
|
55
|
+
? `${scheme}${REDACTION_MARKER}@`
|
|
56
|
+
: REDACTION_MARKER
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
return redacted;
|
|
60
|
+
};
|
|
61
|
+
|
|
36
62
|
export interface ToolResultTruncation {
|
|
37
63
|
content: string;
|
|
38
64
|
omittedLines: number;
|
|
@@ -122,18 +148,35 @@ export const capToolResult = (
|
|
|
122
148
|
};
|
|
123
149
|
};
|
|
124
150
|
|
|
125
|
-
const assistantEntry = (
|
|
151
|
+
const assistantEntry = (
|
|
152
|
+
message: RecordValue,
|
|
153
|
+
policies: AdvisorToolPolicies,
|
|
154
|
+
redact: boolean
|
|
155
|
+
): string | undefined => {
|
|
126
156
|
const parts: string[] = [];
|
|
127
157
|
const text = textFrom(message.content);
|
|
128
158
|
if (text) {
|
|
129
|
-
parts.push(text);
|
|
159
|
+
parts.push(redact ? redactSecrets(text) : text);
|
|
130
160
|
}
|
|
131
161
|
for (const part of contentParts(message.content)) {
|
|
132
162
|
if (!isRecord(part) || part.type !== "toolCall") {
|
|
133
163
|
continue;
|
|
134
164
|
}
|
|
165
|
+
const toolName = typeof part.name === "string" ? part.name : "unknown";
|
|
166
|
+
const policy = policies[toolName] ?? "full";
|
|
167
|
+
if (policy === "exclude") {
|
|
168
|
+
parts.push(`[Tool Call: ${toolName}] (excluded by Advisor tool policy)`);
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
if (policy === "summary") {
|
|
172
|
+
parts.push(
|
|
173
|
+
`[Tool Call: ${toolName}] (arguments omitted by Advisor tool policy: summary)`
|
|
174
|
+
);
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
const argumentsText = JSON.stringify(part.arguments) ?? "undefined";
|
|
135
178
|
parts.push(
|
|
136
|
-
`[Tool Call: ${
|
|
179
|
+
`[Tool Call: ${toolName}(${redact ? redactSecrets(argumentsText) : argumentsText})]`
|
|
137
180
|
);
|
|
138
181
|
}
|
|
139
182
|
return parts.length > 0 ? `Executor: ${parts.join("\n")}` : undefined;
|
|
@@ -142,29 +185,47 @@ const assistantEntry = (message: RecordValue): string | undefined => {
|
|
|
142
185
|
const toolResultEntry = (
|
|
143
186
|
message: RecordValue,
|
|
144
187
|
toolResultMaxLines: number,
|
|
145
|
-
toolResultMaxBytes: number
|
|
188
|
+
toolResultMaxBytes: number,
|
|
189
|
+
policies: AdvisorToolPolicies,
|
|
190
|
+
redact: boolean
|
|
146
191
|
): string => {
|
|
147
|
-
const status = message.isError ? "
|
|
192
|
+
const status = message.isError ? "error" : "success";
|
|
193
|
+
const toolName =
|
|
194
|
+
typeof message.toolName === "string" ? message.toolName : "unknown";
|
|
195
|
+
const policy = policies[toolName] ?? "full";
|
|
196
|
+
const source = textFrom(message.content);
|
|
197
|
+
if (policy === "exclude") {
|
|
198
|
+
return `[Tool Result for ${toolName}] (excluded by Advisor tool policy)`;
|
|
199
|
+
}
|
|
200
|
+
if (policy === "summary") {
|
|
201
|
+
const capped = capToolResult(
|
|
202
|
+
source,
|
|
203
|
+
toolResultMaxLines,
|
|
204
|
+
toolResultMaxBytes
|
|
205
|
+
);
|
|
206
|
+
return `[Tool Result for ${toolName}] (output omitted by Advisor tool policy: summary; status: ${status}; ${capped.totalLines} lines, ${capped.totalBytes} bytes; source output was${capped.truncated ? "" : " not"} truncated)`;
|
|
207
|
+
}
|
|
208
|
+
const disclosed = redact ? redactSecrets(source) : source;
|
|
148
209
|
const capped = capToolResult(
|
|
149
|
-
|
|
210
|
+
disclosed,
|
|
150
211
|
toolResultMaxLines,
|
|
151
212
|
toolResultMaxBytes
|
|
152
213
|
);
|
|
153
|
-
|
|
154
|
-
typeof message.toolName === "string" ? message.toolName : "unknown";
|
|
155
|
-
return `[Tool Result for ${toolName}] (${status}output):\n${capped.content}`;
|
|
214
|
+
return `[Tool Result for ${toolName}] (${message.isError ? "Error " : ""}output):\n${capped.content}`;
|
|
156
215
|
};
|
|
157
216
|
|
|
158
217
|
const conversationEntry = (
|
|
159
218
|
entry: unknown,
|
|
160
219
|
toolResultMaxLines: number,
|
|
161
|
-
toolResultMaxBytes: number
|
|
220
|
+
toolResultMaxBytes: number,
|
|
221
|
+
policies: AdvisorToolPolicies,
|
|
222
|
+
redact: boolean
|
|
162
223
|
): string | undefined => {
|
|
163
224
|
if (!isRecord(entry)) {
|
|
164
225
|
return;
|
|
165
226
|
}
|
|
166
227
|
if (entry.type === "compaction" && typeof entry.summary === "string") {
|
|
167
|
-
return `[System Compaction Summary]: ${entry.summary}`;
|
|
228
|
+
return `[System Compaction Summary]: ${redact ? redactSecrets(entry.summary) : entry.summary}`;
|
|
168
229
|
}
|
|
169
230
|
if (entry.type !== "message" || !isRecord(entry.message)) {
|
|
170
231
|
return;
|
|
@@ -172,13 +233,19 @@ const conversationEntry = (
|
|
|
172
233
|
const { message } = entry;
|
|
173
234
|
if (message.role === "user") {
|
|
174
235
|
const text = textFrom(message.content);
|
|
175
|
-
return text ? `User: ${text}` : undefined;
|
|
236
|
+
return text ? `User: ${redact ? redactSecrets(text) : text}` : undefined;
|
|
176
237
|
}
|
|
177
238
|
if (message.role === "assistant") {
|
|
178
|
-
return assistantEntry(message);
|
|
239
|
+
return assistantEntry(message, policies, redact);
|
|
179
240
|
}
|
|
180
241
|
if (message.role === "toolResult" || message.role === "tool") {
|
|
181
|
-
return toolResultEntry(
|
|
242
|
+
return toolResultEntry(
|
|
243
|
+
message,
|
|
244
|
+
toolResultMaxLines,
|
|
245
|
+
toolResultMaxBytes,
|
|
246
|
+
policies,
|
|
247
|
+
redact
|
|
248
|
+
);
|
|
182
249
|
}
|
|
183
250
|
};
|
|
184
251
|
|
|
@@ -189,10 +256,18 @@ const selectRecentEntries = (entries: string[], maxChars: number): string => {
|
|
|
189
256
|
return joined;
|
|
190
257
|
}
|
|
191
258
|
const selected: string[] = [];
|
|
259
|
+
// Track the joined length instead of re-joining the accumulator each step;
|
|
260
|
+
// long branches with a large maxChars are otherwise quadratic in bytes.
|
|
261
|
+
let selectedLength = 0;
|
|
192
262
|
for (let index = entries.length - 1; index >= 0; index -= 1) {
|
|
193
|
-
const
|
|
194
|
-
|
|
195
|
-
|
|
263
|
+
const entry = entries[index];
|
|
264
|
+
const candidateLength =
|
|
265
|
+
selectedLength +
|
|
266
|
+
entry.length +
|
|
267
|
+
(selected.length === 0 ? 0 : separator.length);
|
|
268
|
+
if (candidateLength <= maxChars || selected.length === 0) {
|
|
269
|
+
selected.unshift(entry);
|
|
270
|
+
selectedLength = candidateLength;
|
|
196
271
|
} else {
|
|
197
272
|
break;
|
|
198
273
|
}
|
|
@@ -208,7 +283,9 @@ export const recentConversation = (
|
|
|
208
283
|
ctx: ExtensionContext,
|
|
209
284
|
maxChars = 15_000,
|
|
210
285
|
toolResultMaxLines = advisorToolResultMaxLinesRef,
|
|
211
|
-
toolResultMaxBytes = advisorToolResultMaxBytesRef
|
|
286
|
+
toolResultMaxBytes = advisorToolResultMaxBytesRef,
|
|
287
|
+
policies = advisorToolPoliciesRef,
|
|
288
|
+
redact = advisorRedactSecretsRef
|
|
212
289
|
): string => {
|
|
213
290
|
if (maxChars === 0) {
|
|
214
291
|
return "";
|
|
@@ -216,7 +293,13 @@ export const recentConversation = (
|
|
|
216
293
|
const entries = ctx.sessionManager
|
|
217
294
|
.getBranch()
|
|
218
295
|
.map((entry) =>
|
|
219
|
-
conversationEntry(
|
|
296
|
+
conversationEntry(
|
|
297
|
+
entry,
|
|
298
|
+
toolResultMaxLines,
|
|
299
|
+
toolResultMaxBytes,
|
|
300
|
+
policies,
|
|
301
|
+
redact
|
|
302
|
+
)
|
|
220
303
|
)
|
|
221
304
|
.filter((entry): entry is string => entry !== undefined);
|
|
222
305
|
return selectRecentEntries(entries, maxChars);
|
package/src/tools.ts
CHANGED
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
advisorRef,
|
|
31
31
|
advisorSessionSummaryRef,
|
|
32
32
|
contextMaxCharsRef,
|
|
33
|
+
isSimpleMode,
|
|
33
34
|
loadConfig,
|
|
34
35
|
splitRef,
|
|
35
36
|
} from "./config.js";
|
|
@@ -69,8 +70,19 @@ export const SPINNER_FRAMES = [
|
|
|
69
70
|
];
|
|
70
71
|
export const resolveAdvisorRequest = (question?: string) =>
|
|
71
72
|
question?.trim() || undefined;
|
|
72
|
-
export const advisorMessageText = (conversation: string, question?: string) =>
|
|
73
|
-
`${conversation ? `<conversation>\n${conversation}\n</conversation>` : ""}${question ? `\n\nTargeted focus:\n${question}` : ""}`;
|
|
73
|
+
export const advisorMessageText = (conversation: string, question?: string) => {
|
|
74
|
+
const text = `${conversation ? `<conversation>\n${conversation}\n</conversation>` : ""}${question ? `\n\nTargeted focus:\n${question}` : ""}`;
|
|
75
|
+
// A zero context limit with no targeted focus would otherwise send an empty
|
|
76
|
+
// user message, which several providers reject outright.
|
|
77
|
+
return (
|
|
78
|
+
text.trim() ||
|
|
79
|
+
"No conversation context is available. State that you cannot review without context."
|
|
80
|
+
);
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
/** The sole reconstructed-context boundary for outgoing Advisor requests. */
|
|
84
|
+
export const advisorRequestConversation = (ctx: ExtensionContext) =>
|
|
85
|
+
recentConversation(ctx, contextMaxCharsRef);
|
|
74
86
|
|
|
75
87
|
export const renderAdvisorCallBox = (
|
|
76
88
|
question: string | undefined,
|
|
@@ -92,6 +104,19 @@ export const renderAdvisorCallBox = (
|
|
|
92
104
|
};
|
|
93
105
|
|
|
94
106
|
const COLLAPSED_ADVICE_LINES = 12;
|
|
107
|
+
// The system prompt requires this exact first line, so the match is exact too.
|
|
108
|
+
const SOUND_VERDICT = /^Verdict:\s*sound$/;
|
|
109
|
+
|
|
110
|
+
export const hasSoundVerdict = (advice: string) =>
|
|
111
|
+
SOUND_VERDICT.test(
|
|
112
|
+
(advice.split("\n").find((line) => line.trim()) ?? "").trim()
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
/** The single Advisor response header shared by tool and manual renderers. */
|
|
116
|
+
export const renderAdvisorResponseHeader = (sound: boolean, theme: Theme) =>
|
|
117
|
+
sound
|
|
118
|
+
? theme.fg("accent", theme.bold("◆ ADVISOR · SOUND"))
|
|
119
|
+
: theme.fg("warning", theme.bold("◆ ADVISOR RESPONSE"));
|
|
95
120
|
|
|
96
121
|
export const adviceForDisplay = (advice: string, expanded: boolean) => {
|
|
97
122
|
if (!advisorCollapseResponsesRef || expanded) {
|
|
@@ -105,6 +130,11 @@ export const adviceForDisplay = (advice: string, expanded: boolean) => {
|
|
|
105
130
|
};
|
|
106
131
|
|
|
107
132
|
export const advisorInvocationGuidelines = () => {
|
|
133
|
+
if (isSimpleMode()) {
|
|
134
|
+
return [
|
|
135
|
+
"When uncertain and normal available tools cannot resolve it, call ask_advisor for a second opinion.",
|
|
136
|
+
];
|
|
137
|
+
}
|
|
108
138
|
const guidelines: string[] = [];
|
|
109
139
|
if (advisorPlanGateRef) {
|
|
110
140
|
guidelines.push(
|
|
@@ -137,6 +167,7 @@ export const ADVISOR_SYSTEM = [
|
|
|
137
167
|
"You already have the relevant reconstructed conversation context. No question or other input from the Executor is needed for a general review.",
|
|
138
168
|
"When no targeted focus is supplied, proactively review the task, risks, proposed direction, and validation from the context. Do not ask the Executor for a question, clarification, more input, or confirmation.",
|
|
139
169
|
"The context may be truncated, so state any material uncertainty and make the best recommendation you can from what is present.",
|
|
170
|
+
"When the implementation is fully sound based on the supplied evidence and you have no material concern or recommended change, begin with exactly `Verdict: sound`. Do not use that verdict when uncertainty, a risk, or a recommendation remains.",
|
|
140
171
|
"You do not act or take over planning. Answer the Executor's request directly in concise, human-readable Markdown. State uncertainty plainly and never claim verification that the supplied evidence does not show.",
|
|
141
172
|
].join(" ");
|
|
142
173
|
|
|
@@ -189,6 +220,7 @@ export const advisorUsageCost = (usage: unknown): number | undefined => {
|
|
|
189
220
|
|
|
190
221
|
const DECISION_LINE = /^Decision\s*:\s*(proceed|revise|blocked)\s*$/i;
|
|
191
222
|
const ANY_DECISION_LINE = /^Decision\s*:\s*(.*?)\s*$/i;
|
|
223
|
+
const CODE_FENCE = /^(?:```|~~~)/;
|
|
192
224
|
const LINE_BREAK = /\r?\n/;
|
|
193
225
|
|
|
194
226
|
export const parseAutomaticDecision = (
|
|
@@ -217,8 +249,19 @@ export const parseAutomaticDecision = (
|
|
|
217
249
|
};
|
|
218
250
|
}
|
|
219
251
|
const decision = match[1].toLowerCase() as GateDecision;
|
|
252
|
+
let insideFence = false;
|
|
220
253
|
for (const line of lines.slice(nonEmpty + 1)) {
|
|
221
|
-
const
|
|
254
|
+
const trimmed = line.trim();
|
|
255
|
+
// A decision quoted inside a fenced example is illustrative, not a second
|
|
256
|
+
// decision, and must not fail the gate.
|
|
257
|
+
if (CODE_FENCE.test(trimmed)) {
|
|
258
|
+
insideFence = !insideFence;
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
if (insideFence) {
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
const subsequent = ANY_DECISION_LINE.exec(trimmed);
|
|
222
265
|
if (!subsequent) {
|
|
223
266
|
continue;
|
|
224
267
|
}
|
|
@@ -272,7 +315,7 @@ const collectAdvisorResponse = async (
|
|
|
272
315
|
throw new Error(`No API key for ${advisorRef}`);
|
|
273
316
|
}
|
|
274
317
|
|
|
275
|
-
const conversation =
|
|
318
|
+
const conversation = advisorRequestConversation(ctx);
|
|
276
319
|
const messages: Message[] = [
|
|
277
320
|
{
|
|
278
321
|
content: [
|
|
@@ -449,7 +492,7 @@ const reserveAdvisorCall = (
|
|
|
449
492
|
session: AdvisorSessionState,
|
|
450
493
|
reservedCalls: Set<string>
|
|
451
494
|
): ToolCallEventResult | undefined => {
|
|
452
|
-
if (event.toolName !== "ask_advisor") {
|
|
495
|
+
if (event.toolName !== "ask_advisor" || isSimpleMode()) {
|
|
453
496
|
return;
|
|
454
497
|
}
|
|
455
498
|
if (!session.canConsult(advisorMaxCallsPerSessionRef)) {
|
|
@@ -517,6 +560,7 @@ const handleAutomaticGate = async (
|
|
|
517
560
|
session: AdvisorSessionState
|
|
518
561
|
): Promise<ToolCallEventResult | undefined> => {
|
|
519
562
|
if (
|
|
563
|
+
isSimpleMode() ||
|
|
520
564
|
event.toolName === "ask_advisor" ||
|
|
521
565
|
!advisorAutoLoopGateRef ||
|
|
522
566
|
!session.recordToolCall(
|
|
@@ -661,7 +705,8 @@ const renderFinalAdvisorResult = (
|
|
|
661
705
|
context.state.timerId = undefined;
|
|
662
706
|
}
|
|
663
707
|
const details = advisorResultDetails(result);
|
|
664
|
-
const
|
|
708
|
+
const advice = details?.text || textFrom(result.content);
|
|
709
|
+
const lines = [renderAdvisorResponseHeader(hasSoundVerdict(advice), theme)];
|
|
665
710
|
if (details?.advisor) {
|
|
666
711
|
lines.push(theme.fg("dim", ` ${details.advisor}`));
|
|
667
712
|
}
|
|
@@ -674,13 +719,15 @@ const renderFinalAdvisorResult = (
|
|
|
674
719
|
)
|
|
675
720
|
);
|
|
676
721
|
}
|
|
677
|
-
const advice
|
|
678
|
-
details?.text ||
|
|
679
|
-
textFrom(result.content) ||
|
|
680
|
-
"(Advisor returned no advice.)";
|
|
722
|
+
const displayAdvice = advice || "(Advisor returned no advice.)";
|
|
681
723
|
box.addChild(new Text(lines.join("\n"), 0, 0));
|
|
682
724
|
box.addChild(
|
|
683
|
-
new Markdown(
|
|
725
|
+
new Markdown(
|
|
726
|
+
adviceForDisplay(displayAdvice, expanded),
|
|
727
|
+
0,
|
|
728
|
+
0,
|
|
729
|
+
getMarkdownTheme()
|
|
730
|
+
)
|
|
684
731
|
);
|
|
685
732
|
};
|
|
686
733
|
|
|
@@ -775,7 +822,9 @@ export const registerAdvisorTool = (pi: ExtensionAPI) => {
|
|
|
775
822
|
}
|
|
776
823
|
loadConfig(ctx);
|
|
777
824
|
const guidelines = advisorInvocationGuidelines();
|
|
778
|
-
const budget =
|
|
825
|
+
const budget = isSimpleMode()
|
|
826
|
+
? undefined
|
|
827
|
+
: session.remainingCalls(advisorMaxCallsPerSessionRef);
|
|
779
828
|
if (budget !== undefined) {
|
|
780
829
|
guidelines.push(
|
|
781
830
|
`Advisor calls remaining this session: ${budget}.\nReserve calls for material decisions, repeated failures, or final review.`
|
|
@@ -793,7 +842,14 @@ export const registerAdvisorTool = (pi: ExtensionAPI) => {
|
|
|
793
842
|
return;
|
|
794
843
|
}
|
|
795
844
|
loadConfig(ctx);
|
|
796
|
-
if (
|
|
845
|
+
if (isSimpleMode()) {
|
|
846
|
+
// Simple mode does not gate, so a block recorded before it was enabled must
|
|
847
|
+
// not linger in session or Herdr state and misreport the session as blocked.
|
|
848
|
+
if (session.blocked) {
|
|
849
|
+
session.clearBlocked();
|
|
850
|
+
herdrAdvisorBlock.clear();
|
|
851
|
+
}
|
|
852
|
+
} else if (session.blocked) {
|
|
797
853
|
return {
|
|
798
854
|
block: true,
|
|
799
855
|
reason: session.blockedReason ?? "Advisor session is blocked.",
|
|
@@ -807,7 +863,7 @@ export const registerAdvisorTool = (pi: ExtensionAPI) => {
|
|
|
807
863
|
});
|
|
808
864
|
|
|
809
865
|
pi.on("agent_settled", (_event, ctx) => {
|
|
810
|
-
if (session.blocked || !advisorSessionSummaryRef) {
|
|
866
|
+
if (isSimpleMode() || session.blocked || !advisorSessionSummaryRef) {
|
|
811
867
|
return;
|
|
812
868
|
}
|
|
813
869
|
const summary = session.summary(advisorMaxCallsPerSessionRef);
|
|
@@ -825,7 +881,7 @@ export const registerAdvisorTool = (pi: ExtensionAPI) => {
|
|
|
825
881
|
description:
|
|
826
882
|
"Consult the on-demand Advisor model for strategic guidance. Call with an empty object for a context-aware review; add question only for a genuinely targeted focus.",
|
|
827
883
|
async execute(_id, params, signal, onUpdate, ctx) {
|
|
828
|
-
if (!reservedCalls.delete(_id)) {
|
|
884
|
+
if (!(reservedCalls.delete(_id) || isSimpleMode())) {
|
|
829
885
|
if (!session.canConsult(advisorMaxCallsPerSessionRef)) {
|
|
830
886
|
throw new Error("Advisor call budget exhausted for this session.");
|
|
831
887
|
}
|