pi-advisor-flow 0.7.0 → 0.8.1-dev.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +27 -18
- package/README.md +9 -8
- package/dist/index.js +1968 -1081
- package/extensions/index.ts +2 -1
- package/package.json +48 -35
- package/src/attachments.ts +11 -10
- package/src/child-session.ts +15 -0
- package/src/commands/activation-preparation.ts +1 -0
- package/src/commands/activation.ts +12 -3
- package/src/commands/lifecycle.ts +7 -1
- package/src/commands/manual-command.ts +7 -1
- package/src/commands/manual-consultation.ts +102 -92
- package/src/commands/manual-progress.ts +7 -5
- package/src/commands/model-commands.ts +1 -1
- package/src/commands/model-options.ts +14 -3
- package/src/commands/model-picker.ts +1 -0
- package/src/commands/registration.ts +1 -0
- package/src/commands/renderers.ts +67 -55
- package/src/commands/runtime.ts +16 -14
- package/src/commands/settings-commands.ts +10 -4
- package/src/commands/settings-persistence.ts +16 -2
- package/src/commands/types.ts +1 -0
- package/src/commands.ts +0 -1
- package/src/config/args.ts +1 -1
- package/src/config/defaults.ts +8 -1
- package/src/config/schema.ts +42 -17
- package/src/config/state.ts +12 -3
- package/src/config/storage.ts +34 -23
- package/src/config/types.ts +16 -3
- package/src/config/validation.ts +33 -21
- package/src/config.ts +3 -0
- package/src/content-utils.ts +24 -4
- package/src/conversation.ts +16 -15
- package/src/git.ts +10 -3
- package/src/herdr-block.ts +83 -0
- package/src/herdr-shared.ts +108 -0
- package/src/herdr.ts +30 -192
- package/src/jev/client.ts +75 -85
- package/src/jev/failure.ts +19 -0
- package/src/jev/key-store.ts +26 -11
- package/src/jev/ledger.ts +58 -64
- package/src/jev/questions.ts +26 -23
- package/src/jev/state.ts +15 -12
- package/src/jev/transport.ts +8 -3
- package/src/model-stream.ts +35 -37
- package/src/outcomes.ts +15 -11
- package/src/pi-settings.ts +4 -9
- package/src/preferences.ts +4 -2
- package/src/redaction.ts +10 -12
- package/src/scout-context.ts +59 -60
- package/src/scout-curation.ts +4 -13
- package/src/scout-groups.ts +178 -175
- package/src/scout-protocol.ts +74 -70
- package/src/scout-reconstruct.ts +1 -1
- package/src/scout.ts +128 -77
- package/src/session-state.ts +64 -49
- package/src/tool-result-cap.ts +18 -17
- package/src/tools/consult-context.ts +7 -15
- package/src/tools/consultation.ts +14 -4
- package/src/tools/gate-policy.ts +1 -0
- package/src/tools/gate-protocol.ts +36 -23
- package/src/tools/jev-filter.ts +43 -41
- package/src/tools/jev-turn-gate.ts +29 -9
- package/src/tools/loop-gate.ts +31 -20
- package/src/tools/model-access.ts +46 -0
- package/src/tools/prompts.ts +20 -25
- package/src/tools/register-ask-advisor.ts +36 -16
- package/src/tools/register-lifecycle.ts +22 -0
- package/src/tools/register-outcome.ts +2 -0
- package/src/tools/register-renderers.ts +146 -128
- package/src/tools/registration.ts +12 -14
- package/src/tools/render-advisor-result.ts +35 -22
- package/src/tools/render-common.ts +16 -19
- package/src/tools/scout-status.ts +42 -27
- package/src/tools/types.ts +1 -0
- package/src/tools.ts +0 -1
- package/src/ui/jev-setup-submenu.ts +65 -59
- package/src/ui/manual-dialog-render.ts +1 -0
- package/src/ui/manual-dialog.ts +39 -43
- package/src/ui/masked-input.ts +5 -11
- package/src/ui/model-multi-selector.ts +9 -0
- package/src/ui/model-selector-adapter.ts +31 -0
- package/src/ui/model-selector.ts +14 -161
- package/src/ui/searchable-model-list.ts +230 -0
- package/src/ui/settings-formatting.ts +24 -14
- package/src/ui/settings-items.ts +38 -1
- package/src/ui/settings-list-adapter.ts +15 -12
- package/src/ui/settings-mutations.ts +157 -70
- package/src/ui/settings-selector.ts +18 -9
- package/src/ui/text-setting-submenu.ts +3 -6
- package/src/ui/types.ts +14 -0
- package/src/ui.ts +0 -1
- package/src/usage.ts +26 -19
package/src/config/storage.ts
CHANGED
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
import { existsSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
} from "
|
|
3
|
+
|
|
4
|
+
import { CONFIG_DIR_NAME, getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
5
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
6
|
+
|
|
7
|
+
import { isRecord, isString } from "../content-utils.ts";
|
|
8
|
+
import type { RecordValue } from "../content-utils.ts";
|
|
8
9
|
import { applyConfig, resetDefaults } from "./defaults.ts";
|
|
9
10
|
import {
|
|
10
11
|
CONFIG_SCHEMA,
|
|
11
12
|
configuredModelRef,
|
|
12
|
-
type PersistedConfigKey,
|
|
13
13
|
SAVED_CONFIG_KEYS,
|
|
14
14
|
} from "./schema.ts";
|
|
15
|
+
import type { PersistedConfigKey } from "./schema.ts";
|
|
15
16
|
import { setAdvisorOutcomeLoggingRef, setPersistedModelRefs } from "./state.ts";
|
|
16
17
|
import type { AdvisorConfig } from "./types.ts";
|
|
17
18
|
import { unknownConfigKeys, validateConfig } from "./validation.ts";
|
|
@@ -29,6 +30,7 @@ type ConfigState = {
|
|
|
29
30
|
};
|
|
30
31
|
|
|
31
32
|
const currentConfigState = (): ConfigState =>
|
|
33
|
+
// SAFETY: SAVED_CONFIG_KEYS enumerates exactly the ConfigState keys, so fromEntries yields ConfigState.
|
|
32
34
|
Object.fromEntries(
|
|
33
35
|
SAVED_CONFIG_KEYS.map((key) => [key, CONFIG_SCHEMA[key].current()])
|
|
34
36
|
) as ConfigState;
|
|
@@ -41,12 +43,10 @@ const sameConfigValue = <Value>(left: Value, right: Value) =>
|
|
|
41
43
|
* preserved like any unknown key, and never trigger the typo warning. */
|
|
42
44
|
const RESERVED_ADVISOR_JSON_KEYS = new Set(["typesafe_api_key"]);
|
|
43
45
|
|
|
44
|
-
export const readExistingConfig = (path: string):
|
|
46
|
+
export const readExistingConfig = (path: string): RecordValue => {
|
|
45
47
|
try {
|
|
46
|
-
const parsed = JSON.parse(readFileSync(path, "
|
|
47
|
-
return parsed
|
|
48
|
-
? (parsed as Record<string, unknown>)
|
|
49
|
-
: {};
|
|
48
|
+
const parsed: unknown = JSON.parse(readFileSync(path, "utf-8"));
|
|
49
|
+
return isRecord(parsed) ? parsed : {};
|
|
50
50
|
} catch {
|
|
51
51
|
return {};
|
|
52
52
|
}
|
|
@@ -61,12 +61,14 @@ const shouldPersistConfigKey = (
|
|
|
61
61
|
(key !== "executor" || persistExecutor);
|
|
62
62
|
|
|
63
63
|
const applyChangedConfigValues = (
|
|
64
|
-
|
|
64
|
+
existing: RecordValue,
|
|
65
65
|
current: ConfigState,
|
|
66
66
|
changedKeys: readonly SavedConfigKey[],
|
|
67
67
|
persistAdvisor: boolean,
|
|
68
68
|
persistExecutor: boolean
|
|
69
|
-
) => {
|
|
69
|
+
): RecordValue => {
|
|
70
|
+
const dropped = new Set<string>();
|
|
71
|
+
const data: RecordValue = { ...existing };
|
|
70
72
|
for (const key of changedKeys) {
|
|
71
73
|
if (!shouldPersistConfigKey(key, persistAdvisor, persistExecutor)) {
|
|
72
74
|
continue;
|
|
@@ -74,11 +76,21 @@ const applyChangedConfigValues = (
|
|
|
74
76
|
const value = current[key];
|
|
75
77
|
const isEmptyModelRef = (key === "advisor" || key === "executor") && !value;
|
|
76
78
|
if (value === undefined || isEmptyModelRef) {
|
|
77
|
-
|
|
79
|
+
dropped.add(key);
|
|
78
80
|
} else {
|
|
79
81
|
data[key] = value;
|
|
80
82
|
}
|
|
81
83
|
}
|
|
84
|
+
if (dropped.size === 0) {
|
|
85
|
+
return data;
|
|
86
|
+
}
|
|
87
|
+
const retained: RecordValue = {};
|
|
88
|
+
for (const key of Object.keys(data)) {
|
|
89
|
+
if (!dropped.has(key)) {
|
|
90
|
+
retained[key] = data[key];
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return retained;
|
|
82
94
|
};
|
|
83
95
|
|
|
84
96
|
// Tracks the last load/save state so a later save merges fresh disk contents instead of overwriting an external edit.
|
|
@@ -86,7 +98,7 @@ let loadedConfigState: ConfigState | undefined;
|
|
|
86
98
|
let loadedConfigPath: string | undefined;
|
|
87
99
|
|
|
88
100
|
const readConfig = (path: string): AdvisorConfig => {
|
|
89
|
-
const config = JSON.parse(readFileSync(path, "
|
|
101
|
+
const config = JSON.parse(readFileSync(path, "utf-8"));
|
|
90
102
|
validateConfig(config, path);
|
|
91
103
|
return config;
|
|
92
104
|
};
|
|
@@ -142,9 +154,9 @@ export const loadConfig = (_ctx: ExtensionContext) => {
|
|
|
142
154
|
);
|
|
143
155
|
if (globalConfig) {
|
|
144
156
|
applyConfig(globalConfig);
|
|
145
|
-
const unknownKeys = unknownConfigKeys(
|
|
146
|
-
|
|
147
|
-
)
|
|
157
|
+
const unknownKeys = unknownConfigKeys(globalConfig).filter(
|
|
158
|
+
(key) => !RESERVED_ADVISOR_JSON_KEYS.has(key)
|
|
159
|
+
);
|
|
148
160
|
const warningIdentity = `${global}:${configIdentity(global)}`;
|
|
149
161
|
if (
|
|
150
162
|
unknownKeys.length > 0 &&
|
|
@@ -196,9 +208,8 @@ export const saveConfig = (
|
|
|
196
208
|
if (changedKeys.length === 0) {
|
|
197
209
|
return path;
|
|
198
210
|
}
|
|
199
|
-
const data =
|
|
200
|
-
|
|
201
|
-
data,
|
|
211
|
+
const data = applyChangedConfigValues(
|
|
212
|
+
existing,
|
|
202
213
|
current,
|
|
203
214
|
changedKeys,
|
|
204
215
|
persistAdvisor,
|
|
@@ -211,14 +222,14 @@ export const saveConfig = (
|
|
|
211
222
|
nextLoadedState.advisor = baseline
|
|
212
223
|
? baseline.advisor
|
|
213
224
|
: configuredModelRef(
|
|
214
|
-
|
|
225
|
+
isString(existing.advisor) ? existing.advisor : undefined
|
|
215
226
|
);
|
|
216
227
|
}
|
|
217
228
|
if (!persistExecutor) {
|
|
218
229
|
nextLoadedState.executor = baseline
|
|
219
230
|
? baseline.executor
|
|
220
231
|
: configuredModelRef(
|
|
221
|
-
|
|
232
|
+
isString(existing.executor) ? existing.executor : undefined
|
|
222
233
|
);
|
|
223
234
|
}
|
|
224
235
|
loadedConfigState = nextLoadedState;
|
package/src/config/types.ts
CHANGED
|
@@ -1,13 +1,23 @@
|
|
|
1
1
|
import {
|
|
2
|
+
DEFAULT_MAX_BYTES as PI_DEFAULT_MAX_BYTES,
|
|
3
|
+
DEFAULT_MAX_LINES as PI_DEFAULT_MAX_LINES,
|
|
4
|
+
} from "@earendil-works/pi-coding-agent";
|
|
5
|
+
|
|
6
|
+
import type { GitContextLevel } from "../git.ts";
|
|
7
|
+
|
|
8
|
+
export {
|
|
2
9
|
DEFAULT_MAX_BYTES,
|
|
3
10
|
DEFAULT_MAX_LINES,
|
|
4
11
|
} from "@earendil-works/pi-coding-agent";
|
|
5
|
-
import type { GitContextLevel } from "../git.ts";
|
|
6
12
|
|
|
7
13
|
export const DEFAULT_CONTEXT_MAX_CHARS = 15_000;
|
|
8
14
|
export const MAX_CONTEXT_MAX_CHARS = Number.MAX_SAFE_INTEGER;
|
|
9
|
-
|
|
10
|
-
|
|
15
|
+
const DEFAULT_ADVISOR_TOOL_RESULT_MAX_LINES = PI_DEFAULT_MAX_LINES;
|
|
16
|
+
const DEFAULT_ADVISOR_TOOL_RESULT_MAX_BYTES = PI_DEFAULT_MAX_BYTES;
|
|
17
|
+
export {
|
|
18
|
+
DEFAULT_ADVISOR_TOOL_RESULT_MAX_BYTES,
|
|
19
|
+
DEFAULT_ADVISOR_TOOL_RESULT_MAX_LINES,
|
|
20
|
+
};
|
|
11
21
|
export const DEFAULT_ADVISOR_GIT_CONTEXT_MAX_CHARS = 20_000;
|
|
12
22
|
export const DEFAULT_JEV_MODEL = "jev-latest";
|
|
13
23
|
// Provisional: the latency benchmark was dropped from scope; revisit if live
|
|
@@ -71,6 +81,7 @@ export interface AdvisorConfig {
|
|
|
71
81
|
advisorJevTurnGateNoulThreshold?: number;
|
|
72
82
|
advisorLoopThreshold?: number;
|
|
73
83
|
advisorMaxCallsPerSession?: number;
|
|
84
|
+
advisorModelWhitelist?: string[];
|
|
74
85
|
advisorOutcomeLogging?: boolean;
|
|
75
86
|
advisorPlanGate?: boolean;
|
|
76
87
|
advisorRedactSecrets?: boolean;
|
|
@@ -90,3 +101,5 @@ export interface AdvisorConfig {
|
|
|
90
101
|
showUsageFooter?: boolean;
|
|
91
102
|
simpleMode?: boolean;
|
|
92
103
|
}
|
|
104
|
+
|
|
105
|
+
export type AdvisorConfigValue = AdvisorConfig[keyof AdvisorConfig];
|
package/src/config/validation.ts
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
1
|
+
import { isBoolean, isRecord, isString } from "../content-utils.ts";
|
|
2
|
+
import type { JsonValue } from "../content-utils.ts";
|
|
3
|
+
import type { ConfigKey, ConfigKeySchema } from "./schema.ts";
|
|
4
|
+
import { configKeys, SCHEMA_BY_KEY } from "./schema.ts";
|
|
3
5
|
import type { AdvisorConfig } from "./types.ts";
|
|
4
6
|
|
|
5
|
-
//
|
|
7
|
+
// Re-exports keep the isValid* validators on their historical module for the config facade.
|
|
6
8
|
export {
|
|
9
|
+
isValidAdvisorModelWhitelist,
|
|
7
10
|
isValidAdvisorToolPolicies,
|
|
8
11
|
isValidContextMaxChars,
|
|
9
12
|
isValidGateFailureMode,
|
|
@@ -13,19 +16,15 @@ export {
|
|
|
13
16
|
isValidToolResultMaxLines,
|
|
14
17
|
} from "./schema.ts";
|
|
15
18
|
|
|
16
|
-
const CONFIG_KEYS = new Set<
|
|
17
|
-
Object.keys(CONFIG_SCHEMA) as ConfigKey[]
|
|
18
|
-
);
|
|
19
|
+
const CONFIG_KEYS = new Set<string>(configKeys);
|
|
19
20
|
|
|
20
21
|
const keysOfType = (type: ConfigKeySchema["type"]): readonly ConfigKey[] =>
|
|
21
|
-
|
|
22
|
-
(key) => SCHEMA_BY_KEY[key].type === type
|
|
23
|
-
);
|
|
22
|
+
configKeys.filter((key) => SCHEMA_BY_KEY[key].type === type);
|
|
24
23
|
|
|
25
24
|
const BOOLEAN_CONFIG_KEYS = keysOfType("boolean");
|
|
26
25
|
const STRING_CONFIG_KEYS = keysOfType("string");
|
|
27
26
|
|
|
28
|
-
type ConfigRecord = Record<string,
|
|
27
|
+
type ConfigRecord = Record<string, JsonValue>;
|
|
29
28
|
|
|
30
29
|
const invalidConfigValue = (
|
|
31
30
|
path: string,
|
|
@@ -37,12 +36,12 @@ const invalidConfigValue = (
|
|
|
37
36
|
);
|
|
38
37
|
};
|
|
39
38
|
|
|
40
|
-
export const unknownConfigKeys = (config:
|
|
41
|
-
Object.keys(config).filter((key) => !CONFIG_KEYS.has(key
|
|
39
|
+
export const unknownConfigKeys = (config: AdvisorConfig) =>
|
|
40
|
+
Object.keys(config).filter((key) => !CONFIG_KEYS.has(key));
|
|
42
41
|
|
|
43
42
|
const validateStringValues = (config: ConfigRecord, path: string) => {
|
|
44
43
|
for (const key of STRING_CONFIG_KEYS) {
|
|
45
|
-
if (config[key] !== undefined &&
|
|
44
|
+
if (config[key] !== undefined && !isString(config[key])) {
|
|
46
45
|
invalidConfigValue(path, key, SCHEMA_BY_KEY[key].accepted);
|
|
47
46
|
}
|
|
48
47
|
}
|
|
@@ -50,7 +49,7 @@ const validateStringValues = (config: ConfigRecord, path: string) => {
|
|
|
50
49
|
|
|
51
50
|
const validateBooleanValues = (config: ConfigRecord, path: string) => {
|
|
52
51
|
for (const key of BOOLEAN_CONFIG_KEYS) {
|
|
53
|
-
if (config[key] !== undefined &&
|
|
52
|
+
if (config[key] !== undefined && !isBoolean(config[key])) {
|
|
54
53
|
invalidConfigValue(path, key, SCHEMA_BY_KEY[key].accepted);
|
|
55
54
|
}
|
|
56
55
|
}
|
|
@@ -69,6 +68,19 @@ const validateNumericValues = (config: ConfigRecord, path: string) => {
|
|
|
69
68
|
}
|
|
70
69
|
};
|
|
71
70
|
|
|
71
|
+
const validateArrayValues = (config: ConfigRecord, path: string) => {
|
|
72
|
+
for (const key of keysOfType("array")) {
|
|
73
|
+
const isValid = SCHEMA_BY_KEY[key].validate;
|
|
74
|
+
if (
|
|
75
|
+
config[key] !== undefined &&
|
|
76
|
+
isValid !== undefined &&
|
|
77
|
+
!isValid(config[key])
|
|
78
|
+
) {
|
|
79
|
+
invalidConfigValue(path, key, SCHEMA_BY_KEY[key].accepted);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
|
|
72
84
|
const validateEnumValues = (config: ConfigRecord, path: string) => {
|
|
73
85
|
for (const key of keysOfType("enum")) {
|
|
74
86
|
const isValid = SCHEMA_BY_KEY[key].validate;
|
|
@@ -99,16 +111,16 @@ export const validateConfig = (
|
|
|
99
111
|
value: unknown,
|
|
100
112
|
path = "advisor.json"
|
|
101
113
|
): value is AdvisorConfig => {
|
|
102
|
-
if (!
|
|
114
|
+
if (!isRecord(value)) {
|
|
103
115
|
throw new TypeError(
|
|
104
116
|
`Invalid advisor configuration at ${path}: expected a JSON object.`
|
|
105
117
|
);
|
|
106
118
|
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
validateEnumValues(
|
|
119
|
+
validateStringValues(value, path);
|
|
120
|
+
validateBooleanValues(value, path);
|
|
121
|
+
validateNumericValues(value, path);
|
|
122
|
+
validateObjectValues(value, path);
|
|
123
|
+
validateArrayValues(value, path);
|
|
124
|
+
validateEnumValues(value, path);
|
|
113
125
|
return true;
|
|
114
126
|
};
|
package/src/config.ts
CHANGED
|
@@ -14,6 +14,7 @@ export {
|
|
|
14
14
|
advisorHerdrIntegrationRef,
|
|
15
15
|
advisorLoopThresholdRef,
|
|
16
16
|
advisorMaxCallsPerSessionRef,
|
|
17
|
+
advisorModelWhitelistRef,
|
|
17
18
|
advisorOutcomeLoggingRef,
|
|
18
19
|
advisorPlanGateRef,
|
|
19
20
|
advisorRedactSecretsRef,
|
|
@@ -46,6 +47,7 @@ export {
|
|
|
46
47
|
setAdvisorHerdrIntegrationRef,
|
|
47
48
|
setAdvisorLoopThresholdRef,
|
|
48
49
|
setAdvisorMaxCallsPerSessionRef,
|
|
50
|
+
setAdvisorModelWhitelistRef,
|
|
49
51
|
setAdvisorOutcomeLoggingRef,
|
|
50
52
|
setAdvisorPlanGateRef,
|
|
51
53
|
setAdvisorRedactSecretsRef,
|
|
@@ -93,6 +95,7 @@ export {
|
|
|
93
95
|
MAX_CONTEXT_MAX_CHARS,
|
|
94
96
|
} from "./config/types.ts";
|
|
95
97
|
export {
|
|
98
|
+
isValidAdvisorModelWhitelist,
|
|
96
99
|
isValidAdvisorToolPolicies,
|
|
97
100
|
isValidContextMaxChars,
|
|
98
101
|
isValidGateFailureMode,
|
package/src/content-utils.ts
CHANGED
|
@@ -1,14 +1,34 @@
|
|
|
1
1
|
/** Shared untyped-content inspection helpers. */
|
|
2
2
|
|
|
3
|
-
export type
|
|
3
|
+
export type JsonValue =
|
|
4
|
+
| null
|
|
5
|
+
| boolean
|
|
6
|
+
| number
|
|
7
|
+
| string
|
|
8
|
+
| JsonValue[]
|
|
9
|
+
| { [key: string]: JsonValue };
|
|
10
|
+
|
|
11
|
+
export type RecordValue = Record<string, JsonValue>;
|
|
4
12
|
|
|
5
13
|
export const isRecord = (value: unknown): value is RecordValue =>
|
|
6
14
|
Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
7
15
|
|
|
8
|
-
export const
|
|
16
|
+
export const isRecordOf = <Value>(value: Value): value is Value & RecordValue =>
|
|
17
|
+
Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
18
|
+
|
|
19
|
+
export const isString = <Value>(value: Value): value is Value & string =>
|
|
20
|
+
typeof value === "string";
|
|
21
|
+
|
|
22
|
+
export const isNumber = <Value>(value: Value): value is Value & number =>
|
|
23
|
+
typeof value === "number";
|
|
24
|
+
|
|
25
|
+
export const isBoolean = <Value>(value: Value): value is Value & boolean =>
|
|
26
|
+
typeof value === "boolean";
|
|
27
|
+
|
|
28
|
+
export const byteLength = (value: string) => Buffer.byteLength(value, "utf-8");
|
|
9
29
|
|
|
10
|
-
export const contentParts = (content:
|
|
11
|
-
if (
|
|
30
|
+
export const contentParts = <Content>(content: Content): unknown[] => {
|
|
31
|
+
if (isString(content)) {
|
|
12
32
|
return [content];
|
|
13
33
|
}
|
|
14
34
|
return Array.isArray(content) ? content : [];
|
package/src/conversation.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
|
|
2
3
|
import {
|
|
3
4
|
advisorRedactSecretsRef,
|
|
4
5
|
advisorToolPoliciesRef,
|
|
@@ -6,21 +7,22 @@ import {
|
|
|
6
7
|
advisorToolResultMaxLinesRef,
|
|
7
8
|
} from "./config/state.ts";
|
|
8
9
|
import type { AdvisorToolPolicies } from "./config/types.ts";
|
|
9
|
-
import { contentParts,
|
|
10
|
+
import { contentParts, isRecordOf, isString } from "./content-utils.ts";
|
|
11
|
+
import type { RecordValue } from "./content-utils.ts";
|
|
10
12
|
import { redactSecrets } from "./redaction.ts";
|
|
11
13
|
import { capToolResult } from "./tool-result-cap.ts";
|
|
12
14
|
|
|
13
|
-
const textFromPart = (part:
|
|
14
|
-
if (
|
|
15
|
+
const textFromPart = <Part>(part: Part): string => {
|
|
16
|
+
if (isString(part)) {
|
|
15
17
|
return part;
|
|
16
18
|
}
|
|
17
|
-
if (!
|
|
19
|
+
if (!isRecordOf(part) || part.type !== "text") {
|
|
18
20
|
return "";
|
|
19
21
|
}
|
|
20
|
-
return
|
|
22
|
+
return isString(part.text) ? part.text : "";
|
|
21
23
|
};
|
|
22
24
|
|
|
23
|
-
export const textFrom = (content:
|
|
25
|
+
export const textFrom = <Content>(content: Content): string =>
|
|
24
26
|
contentParts(content).map(textFromPart).join("\n").trim();
|
|
25
27
|
|
|
26
28
|
const assistantEntry = (
|
|
@@ -34,10 +36,10 @@ const assistantEntry = (
|
|
|
34
36
|
parts.push(redact ? redactSecrets(text) : text);
|
|
35
37
|
}
|
|
36
38
|
for (const part of contentParts(message.content)) {
|
|
37
|
-
if (!
|
|
39
|
+
if (!isRecordOf(part) || part.type !== "toolCall") {
|
|
38
40
|
continue;
|
|
39
41
|
}
|
|
40
|
-
const toolName =
|
|
42
|
+
const toolName = isString(part.name) ? part.name : "unknown";
|
|
41
43
|
const policy = policies[toolName] ?? "full";
|
|
42
44
|
if (policy === "exclude") {
|
|
43
45
|
parts.push(`[Tool Call: ${toolName}] (excluded by Advisor tool policy)`);
|
|
@@ -65,8 +67,7 @@ const toolResultEntry = (
|
|
|
65
67
|
redact: boolean
|
|
66
68
|
): string => {
|
|
67
69
|
const status = message.isError ? "error" : "success";
|
|
68
|
-
const toolName =
|
|
69
|
-
typeof message.toolName === "string" ? message.toolName : "unknown";
|
|
70
|
+
const toolName = isString(message.toolName) ? message.toolName : "unknown";
|
|
70
71
|
const policy = policies[toolName] ?? "full";
|
|
71
72
|
const source = textFrom(message.content);
|
|
72
73
|
if (policy === "exclude") {
|
|
@@ -89,20 +90,20 @@ const toolResultEntry = (
|
|
|
89
90
|
return `[Tool Result for ${toolName}] (${message.isError ? "Error " : ""}output):\n${capped.content}`;
|
|
90
91
|
};
|
|
91
92
|
|
|
92
|
-
export const conversationEntry = (
|
|
93
|
-
entry:
|
|
93
|
+
export const conversationEntry = <Entry>(
|
|
94
|
+
entry: Entry,
|
|
94
95
|
toolResultMaxLines: number,
|
|
95
96
|
toolResultMaxBytes: number,
|
|
96
97
|
policies: AdvisorToolPolicies,
|
|
97
98
|
redact: boolean
|
|
98
99
|
): string | undefined => {
|
|
99
|
-
if (!
|
|
100
|
+
if (!isRecordOf(entry)) {
|
|
100
101
|
return;
|
|
101
102
|
}
|
|
102
|
-
if (entry.type === "compaction" &&
|
|
103
|
+
if (entry.type === "compaction" && isString(entry.summary)) {
|
|
103
104
|
return `[System Compaction Summary]: ${redact ? redactSecrets(entry.summary) : entry.summary}`;
|
|
104
105
|
}
|
|
105
|
-
if (entry.type !== "message" || !
|
|
106
|
+
if (entry.type !== "message" || !isRecordOf(entry.message)) {
|
|
106
107
|
return;
|
|
107
108
|
}
|
|
108
109
|
const { message } = entry;
|
package/src/git.ts
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
2
|
|
|
3
|
+
import { isString } from "./content-utils.ts";
|
|
4
|
+
|
|
3
5
|
/** How much repository change context may leave the machine. */
|
|
4
6
|
export type GitContextLevel = "off" | "summary" | "full";
|
|
5
7
|
|
|
6
8
|
export const GIT_CONTEXT_LEVELS: GitContextLevel[] = ["off", "summary", "full"];
|
|
7
9
|
|
|
10
|
+
const GIT_CONTEXT_LEVEL_SET = new Set<string>(GIT_CONTEXT_LEVELS);
|
|
11
|
+
|
|
8
12
|
export const isValidGitContextLevel = (
|
|
9
13
|
value: unknown
|
|
10
14
|
): value is GitContextLevel =>
|
|
11
|
-
|
|
15
|
+
isString(value) && GIT_CONTEXT_LEVEL_SET.has(value);
|
|
12
16
|
|
|
13
17
|
const LEVEL_RANK: Record<GitContextLevel, number> = {
|
|
14
18
|
full: 2,
|
|
@@ -47,7 +51,10 @@ export interface GitContextResult {
|
|
|
47
51
|
* would end the region early and promote the remainder to trusted instructions.
|
|
48
52
|
*/
|
|
49
53
|
export const escapeRepositoryText = (value: string) =>
|
|
50
|
-
value
|
|
54
|
+
value
|
|
55
|
+
.replaceAll("&", "&")
|
|
56
|
+
.replaceAll("<", "<")
|
|
57
|
+
.replaceAll(">", ">");
|
|
51
58
|
|
|
52
59
|
const TRUNCATION_NOTICE =
|
|
53
60
|
"\n[Repository context truncated: it exceeded the configured limit.]";
|
|
@@ -84,7 +91,7 @@ const deadlineRunner = (): GitRunner => {
|
|
|
84
91
|
}
|
|
85
92
|
return execFileSync("git", args, {
|
|
86
93
|
cwd,
|
|
87
|
-
encoding: "
|
|
94
|
+
encoding: "utf-8",
|
|
88
95
|
maxBuffer: GIT_MAX_BUFFER,
|
|
89
96
|
// Never use a shell: arguments are fixed and must not be re-parsed.
|
|
90
97
|
shell: false,
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { Report } from "./herdr-shared.ts";
|
|
2
|
+
import {
|
|
3
|
+
BLOCK_SOURCE,
|
|
4
|
+
cleanNotification,
|
|
5
|
+
HERDR_PI_SOURCE,
|
|
6
|
+
nextSequence,
|
|
7
|
+
safeEmitBlocked,
|
|
8
|
+
sendToHerdr,
|
|
9
|
+
} from "./herdr-shared.ts";
|
|
10
|
+
|
|
11
|
+
export class HerdrAdvisorBlock {
|
|
12
|
+
#blocked = false;
|
|
13
|
+
private readonly report: Report;
|
|
14
|
+
private readonly enabled: () => boolean;
|
|
15
|
+
|
|
16
|
+
constructor(
|
|
17
|
+
report: Report = sendToHerdr,
|
|
18
|
+
enabled: () => boolean = () => true
|
|
19
|
+
) {
|
|
20
|
+
this.report = report;
|
|
21
|
+
this.enabled = enabled;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
set(reason: string) {
|
|
25
|
+
if (!this.enabled()) {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
const label = cleanNotification(reason, 200);
|
|
29
|
+
const wasBlocked: boolean = this.#blocked;
|
|
30
|
+
if (!wasBlocked) {
|
|
31
|
+
// The listener refcounts, so only the false → true edge may emit.
|
|
32
|
+
safeEmitBlocked(true, label);
|
|
33
|
+
}
|
|
34
|
+
this.#blocked = true;
|
|
35
|
+
this.safeReport({ blocked: label });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
clear() {
|
|
39
|
+
const wasBlocked: boolean = this.#blocked;
|
|
40
|
+
this.#blocked = false;
|
|
41
|
+
if (!wasBlocked) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
// Clearing previously reported state is a de-escalation and must still be
|
|
45
|
+
// delivered if integration was disabled after the block was reported.
|
|
46
|
+
safeEmitBlocked(false);
|
|
47
|
+
try {
|
|
48
|
+
this.report({
|
|
49
|
+
id: `${BLOCK_SOURCE}:${nextSequence()}`,
|
|
50
|
+
method: "pane.report_metadata",
|
|
51
|
+
params: {
|
|
52
|
+
agent: "pi",
|
|
53
|
+
applies_to_source: HERDR_PI_SOURCE,
|
|
54
|
+
clear_state_labels: true,
|
|
55
|
+
pane_id: process.env.HERDR_PANE_ID ?? "",
|
|
56
|
+
seq: nextSequence(),
|
|
57
|
+
source: BLOCK_SOURCE,
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
} catch {
|
|
61
|
+
/* Herdr is optional. */
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
private safeReport(labels: { blocked: string }) {
|
|
66
|
+
try {
|
|
67
|
+
this.report({
|
|
68
|
+
id: `${BLOCK_SOURCE}:${nextSequence()}`,
|
|
69
|
+
method: "pane.report_metadata",
|
|
70
|
+
params: {
|
|
71
|
+
agent: "pi",
|
|
72
|
+
applies_to_source: HERDR_PI_SOURCE,
|
|
73
|
+
pane_id: process.env.HERDR_PANE_ID ?? "",
|
|
74
|
+
seq: nextSequence(),
|
|
75
|
+
source: BLOCK_SOURCE,
|
|
76
|
+
state_labels: labels,
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
} catch {
|
|
80
|
+
/* Herdr is optional. */
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import net from "node:net";
|
|
2
|
+
|
|
3
|
+
import { redactSecrets } from "./redaction.ts";
|
|
4
|
+
|
|
5
|
+
// Keep the JSON-RPC method components separate from Socket's URL-string heuristic.
|
|
6
|
+
const HERDR_NOTIFICATION_METHOD = "notification.show";
|
|
7
|
+
|
|
8
|
+
export const SOURCE = "pi-advisor:advisor-activity";
|
|
9
|
+
const NOTIFICATION_SOURCE = "pi-advisor:advisor-notification";
|
|
10
|
+
export const BLOCK_SOURCE = "pi-advisor:advisor-block";
|
|
11
|
+
export const HERDR_PI_SOURCE = "herdr:pi";
|
|
12
|
+
|
|
13
|
+
export interface HerdrMetadataRequest {
|
|
14
|
+
id: string;
|
|
15
|
+
method: "pane.report_metadata";
|
|
16
|
+
params: {
|
|
17
|
+
pane_id: string;
|
|
18
|
+
source: string;
|
|
19
|
+
agent: "pi";
|
|
20
|
+
applies_to_source: string;
|
|
21
|
+
state_labels?: { working?: string; blocked?: string };
|
|
22
|
+
clear_state_labels?: true;
|
|
23
|
+
seq: number;
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface HerdrNotificationRequest {
|
|
28
|
+
id: string;
|
|
29
|
+
method: typeof HERDR_NOTIFICATION_METHOD;
|
|
30
|
+
params: {
|
|
31
|
+
title: string;
|
|
32
|
+
body: string;
|
|
33
|
+
position: "top-left";
|
|
34
|
+
sound: "request";
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export type HerdrRequest = HerdrMetadataRequest | HerdrNotificationRequest;
|
|
39
|
+
|
|
40
|
+
export type Report = (request: HerdrRequest) => void;
|
|
41
|
+
|
|
42
|
+
let sequence = Date.now() * 1000;
|
|
43
|
+
export const nextSequence = () => {
|
|
44
|
+
sequence += 1;
|
|
45
|
+
return sequence;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// Herdr drops socket reports from other sources, so the blocked state must be
|
|
49
|
+
// signalled through the in-process pi event bus its integration listens on.
|
|
50
|
+
type BlockedEmitter = (active: boolean, label: string) => void;
|
|
51
|
+
let emitBlocked: BlockedEmitter | undefined;
|
|
52
|
+
|
|
53
|
+
export const setHerdrBlockedEmitter = (emitter: BlockedEmitter | undefined) => {
|
|
54
|
+
emitBlocked = emitter;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export const safeEmitBlocked = (active: boolean, label = "Advisor blocked") => {
|
|
58
|
+
try {
|
|
59
|
+
emitBlocked?.(active, label);
|
|
60
|
+
} catch {
|
|
61
|
+
/* Herdr is optional. */
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const isControlCharacter = (character: string) =>
|
|
66
|
+
character <= "\u001F" || character === "\u007F";
|
|
67
|
+
|
|
68
|
+
export const cleanNotification = (value: string, max: number) =>
|
|
69
|
+
[...redactSecrets(value)]
|
|
70
|
+
.map((character) => (isControlCharacter(character) ? " " : character))
|
|
71
|
+
.join("")
|
|
72
|
+
.replaceAll(/\s+/gu, " ")
|
|
73
|
+
.trim()
|
|
74
|
+
.slice(0, max);
|
|
75
|
+
|
|
76
|
+
export const sendToHerdr: Report = (request) => {
|
|
77
|
+
if (process.env.HERDR_ENV !== "1") {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const paneId = process.env.HERDR_PANE_ID;
|
|
81
|
+
const socketPath = process.env.HERDR_SOCKET_PATH;
|
|
82
|
+
if (!(paneId && socketPath)) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
const endpoint =
|
|
86
|
+
process.platform === "win32" ? `\\\\.\\pipe\\${socketPath}` : socketPath;
|
|
87
|
+
const socket = net.createConnection(endpoint);
|
|
88
|
+
const timeout = setTimeout(() => socket.destroy(), 500);
|
|
89
|
+
timeout.unref?.();
|
|
90
|
+
socket.once("connect", () => socket.write(`${JSON.stringify(request)}\n`));
|
|
91
|
+
socket.once("data", () => socket.destroy());
|
|
92
|
+
socket.once("error", () => socket.destroy());
|
|
93
|
+
socket.once("close", () => clearTimeout(timeout));
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
export const createHerdrNotificationRequest = (
|
|
97
|
+
title: string,
|
|
98
|
+
body: string
|
|
99
|
+
): HerdrNotificationRequest => ({
|
|
100
|
+
id: `${NOTIFICATION_SOURCE}:${nextSequence()}`,
|
|
101
|
+
method: HERDR_NOTIFICATION_METHOD,
|
|
102
|
+
params: {
|
|
103
|
+
body: cleanNotification(body, 240),
|
|
104
|
+
position: "top-left",
|
|
105
|
+
sound: "request",
|
|
106
|
+
title: cleanNotification(title, 80),
|
|
107
|
+
},
|
|
108
|
+
});
|