pi-pier 0.3.0 → 0.3.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/README.md +12 -11
- package/package.json +2 -2
- package/schemas/role-manifest.schema.json +7 -1
- package/skills/pi-herdr-workflow/SKILL.md +4 -5
- package/src/ansi-text.ts +62 -11
- package/src/ask-dialog.ts +308 -0
- package/src/ask-user.ts +113 -112
- package/src/bootstrap.ts +8 -12
- package/src/compact-coordinator.ts +278 -261
- package/src/compact-economics-core.ts +77 -145
- package/src/config-catalog-core.ts +77 -197
- package/src/config-command.ts +271 -64
- package/src/dashboard-command.ts +38 -19
- package/src/efficiency-config-core.ts +287 -484
- package/src/efficiency-store.ts +16 -29
- package/src/focus-poller.ts +14 -35
- package/src/gc-core.ts +10 -36
- package/src/herdr-client.ts +82 -208
- package/src/history-store.ts +36 -23
- package/src/index-gates.ts +124 -0
- package/src/index-locks.ts +2 -7
- package/src/index-master.ts +24 -18
- package/src/index-notices.ts +28 -15
- package/src/index-pipe.ts +17 -12
- package/src/index-roles.ts +306 -0
- package/src/index-runtime.ts +2 -6
- package/src/index-worker.ts +4 -8
- package/src/index.ts +305 -564
- package/src/jev-client.ts +12 -17
- package/src/jev-core.ts +137 -87
- package/src/ledger.ts +13 -21
- package/src/lock-core.ts +28 -56
- package/src/manifest-compose.ts +35 -20
- package/src/observation-core.ts +52 -100
- package/src/pane-title.ts +19 -38
- package/src/pi-surface.ts +34 -22
- package/src/pier-options.ts +54 -50
- package/src/pipe-channel.ts +27 -59
- package/src/platform-paths.ts +19 -52
- package/src/{core → plugins}/grid-shape.ts +8 -10
- package/src/{core → plugins}/heat-plan.ts +20 -22
- package/src/plugins/observation.ts +475 -0
- package/src/plugins/subagent.ts +728 -0
- package/src/{core → plugins}/terminal.ts +167 -127
- package/src/{core → plugins}/todo.ts +61 -100
- package/src/progress-core.ts +16 -53
- package/src/reconcile-core.ts +24 -33
- package/src/reducer-core.ts +24 -60
- package/src/reducer-invoker.ts +266 -239
- package/src/renderers.ts +76 -87
- package/src/role-loader.ts +33 -28
- package/src/role-manifest.ts +23 -28
- package/src/role-state.ts +119 -0
- package/src/routing-telemetry.ts +136 -0
- package/src/runtime-policy.ts +25 -40
- package/src/session-tail.ts +65 -41
- package/src/settle-wake-core.ts +52 -21
- package/src/slim-frame.ts +25 -122
- package/src/stale-core.ts +17 -24
- package/src/storage-layout.ts +14 -43
- package/src/subagent-core.ts +253 -118
- package/src/subagent-gc.ts +109 -67
- package/src/subagent-poller.ts +367 -24
- package/src/subagent-session.ts +516 -0
- package/src/subagent-spawn.ts +530 -52
- package/src/swallow.ts +7 -17
- package/src/terminal-core.ts +125 -106
- package/src/todo-core.ts +57 -109
- package/src/todo-read-hook.ts +23 -39
- package/src/todo-reminder-core.ts +26 -51
- package/src/todo-window.ts +11 -12
- package/src/todos-service.ts +15 -35
- package/src/tool-error.ts +6 -13
- package/src/tool-gate.ts +38 -21
- package/src/vocab.ts +52 -25
- package/src/ask-multi.ts +0 -260
- package/src/config-guide.ts +0 -361
- package/src/core/observation.ts +0 -555
- package/src/core/subagent.ts +0 -633
- package/src/efficiency-config-io.ts +0 -53
- package/src/git-adapter.ts +0 -113
- package/src/notice-buffer.ts +0 -23
- package/src/subagent-execute.ts +0 -328
- package/src/subagent-git-io.ts +0 -80
- package/src/subagent-launch.ts +0 -114
- package/src/subagent-list-action.ts +0 -69
- package/src/subagent-output-action.ts +0 -104
- package/src/subagent-output-adapter.ts +0 -27
- package/src/subagent-output-core.ts +0 -263
- package/src/subagent-poll-loop.ts +0 -305
- package/src/subagent-port.ts +0 -27
- package/src/subagent-ready.ts +0 -89
- package/src/subagent-registry.ts +0 -70
- package/src/subagent-resolution.ts +0 -41
- package/src/subagent-scope.ts +0 -41
- package/src/subagent-session-io.ts +0 -230
|
@@ -1,21 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* D100-D103
|
|
2
|
+
* D100-D103 efficiency configuration: schema, validation, multi-tier resolution and the two files
|
|
3
|
+
* pier itself reads for it.
|
|
3
4
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* - Project-level configuration requires `isProjectTrusted: true` (security boundary).
|
|
10
|
-
* - Workspace config shallow-replaces user config.
|
|
11
|
-
* - Fail-open: configuration parse/validation errors disable the affected mechanism
|
|
12
|
-
* and emit a single-line warning, never crashing normal agent sessions.
|
|
13
|
-
* - Environment variables (PI_HERDR_*) take highest precedence.
|
|
5
|
+
* Contract: unknown keys are rejected (typo guard — all issues collected in one pass); a project-level
|
|
6
|
+
* config requires `isProjectTrusted: true` (security boundary); workspace config shallow-replaces user
|
|
7
|
+
* config while env (`PI_HERDR_*` / `PIER_JEV_*`) wins over both; and a config that fails validation
|
|
8
|
+
* disables the affected mechanism with a one-line warning instead of crashing the session (an unknown
|
|
9
|
+
* top-level key disables every mechanism).
|
|
14
10
|
*/
|
|
15
11
|
|
|
12
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
16
13
|
import { homedir } from 'node:os';
|
|
17
14
|
import { join } from 'node:path';
|
|
18
|
-
import { loadPiNativeCompactionSettings, readJsonConfig } from './efficiency-config-io.ts';
|
|
19
15
|
|
|
20
16
|
export interface OnlineContextCompactConfig {
|
|
21
17
|
enabled: boolean;
|
|
@@ -47,9 +43,8 @@ export interface EvidencePreservingReducerConfig {
|
|
|
47
43
|
}
|
|
48
44
|
|
|
49
45
|
/**
|
|
50
|
-
* Jev decision layer (RFC docs/rfc-jev-integration.md)
|
|
51
|
-
*
|
|
52
|
-
* fails open to the existing heuristic on any error, timeout, or low confidence.
|
|
46
|
+
* Jev decision layer (RFC docs/rfc-jev-integration.md): optional System One classification calls that
|
|
47
|
+
* upgrade hand-written heuristics; every call site fails open on any error, timeout, or low confidence.
|
|
53
48
|
*/
|
|
54
49
|
export interface JevConfig {
|
|
55
50
|
enabled: boolean;
|
|
@@ -77,89 +72,141 @@ export interface EfficiencyConfig {
|
|
|
77
72
|
export const DEFAULT_EFFICIENCY_CONFIG: EfficiencyConfig = Object.freeze({
|
|
78
73
|
version: 1,
|
|
79
74
|
onlineContextCompact: Object.freeze({
|
|
80
|
-
enabled: false,
|
|
81
|
-
|
|
82
|
-
cacheWriteReadRatio: 'auto',
|
|
83
|
-
keepRecentTokens: 20000,
|
|
84
|
-
firstCompactionRequestScale: 2.0,
|
|
85
|
-
subsequentCompactionMargin: 1.5,
|
|
75
|
+
enabled: false, logEnabled: false, cacheWriteReadRatio: 'auto', keepRecentTokens: 20000,
|
|
76
|
+
firstCompactionRequestScale: 2.0, subsequentCompactionMargin: 1.5,
|
|
86
77
|
}),
|
|
87
78
|
observationPack: Object.freeze({
|
|
88
|
-
enabled: false,
|
|
89
|
-
|
|
90
|
-
thresholdBytes: 10240,
|
|
91
|
-
fullSends: 2,
|
|
92
|
-
recallChunkBytes: 16384,
|
|
93
|
-
excerptBytes: 1024,
|
|
79
|
+
enabled: false, logEnabled: false, thresholdBytes: 10240, fullSends: 2,
|
|
80
|
+
recallChunkBytes: 16384, excerptBytes: 1024,
|
|
94
81
|
}),
|
|
95
82
|
evidencePreservingReducer: Object.freeze({
|
|
96
|
-
enabled: false,
|
|
97
|
-
|
|
98
|
-
minBytes: 4096,
|
|
99
|
-
maxChars: 600000,
|
|
100
|
-
maxOutputTokens: 2048,
|
|
101
|
-
timeoutMs: 5000,
|
|
102
|
-
localOnly: false,
|
|
103
|
-
}),
|
|
104
|
-
jev: Object.freeze({
|
|
105
|
-
enabled: false,
|
|
106
|
-
logEnabled: false,
|
|
107
|
-
model: 'jev-1.13.0',
|
|
108
|
-
timeoutMs: 2000,
|
|
109
|
-
minConfidence: 0.6,
|
|
83
|
+
enabled: false, logEnabled: false, minBytes: 4096, maxChars: 600000,
|
|
84
|
+
maxOutputTokens: 2048, timeoutMs: 5000, localOnly: false,
|
|
110
85
|
}),
|
|
86
|
+
jev: Object.freeze({ enabled: false, logEnabled: false, model: 'jev-1.13.0', timeoutMs: 2000, minConfidence: 0.6 }),
|
|
111
87
|
});
|
|
112
88
|
|
|
113
89
|
function isPlainObject(v: unknown): v is Record<string, unknown> {
|
|
114
90
|
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
115
91
|
}
|
|
116
92
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
'
|
|
132
|
-
]
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
'
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
'
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
93
|
+
type FieldKind = 'bool' | 'int' | 'number' | 'ratio' | 'string' | 'optionalString';
|
|
94
|
+
|
|
95
|
+
interface FieldSpec {
|
|
96
|
+
readonly key: string;
|
|
97
|
+
readonly kind: FieldKind;
|
|
98
|
+
/** Lower bound for `int`/`number`; also printed in the issue message. */
|
|
99
|
+
readonly min?: number;
|
|
100
|
+
readonly max?: number;
|
|
101
|
+
/** Issue text for a bound that must not be printed as a plain JS number ("1.0"). */
|
|
102
|
+
readonly minText?: string;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** A section is `{enabled, ...fields}`; field order fixes the order of reported issues. */
|
|
106
|
+
interface SectionSpec {
|
|
107
|
+
readonly name: 'onlineContextCompact' | 'observationPack' | 'evidencePreservingReducer' | 'jev';
|
|
108
|
+
readonly fields: readonly FieldSpec[];
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const SECTIONS: readonly SectionSpec[] = [
|
|
112
|
+
{ name: 'onlineContextCompact', fields: [
|
|
113
|
+
{ key: 'enabled', kind: 'bool' },
|
|
114
|
+
{ key: 'logEnabled', kind: 'bool' },
|
|
115
|
+
{ key: 'cacheWriteReadRatio', kind: 'ratio' },
|
|
116
|
+
{ key: 'keepRecentTokens', kind: 'int', min: 1000 },
|
|
117
|
+
{ key: 'firstCompactionRequestScale', kind: 'number', min: 1, minText: '1.0' },
|
|
118
|
+
{ key: 'subsequentCompactionMargin', kind: 'number', min: 1, minText: '1.0' },
|
|
119
|
+
] },
|
|
120
|
+
{ name: 'observationPack', fields: [
|
|
121
|
+
{ key: 'enabled', kind: 'bool' },
|
|
122
|
+
{ key: 'logEnabled', kind: 'bool' },
|
|
123
|
+
{ key: 'thresholdBytes', kind: 'int', min: 1024 },
|
|
124
|
+
{ key: 'fullSends', kind: 'int', min: 1 },
|
|
125
|
+
{ key: 'recallChunkBytes', kind: 'int', min: 1024 },
|
|
126
|
+
{ key: 'excerptBytes', kind: 'int', min: 128 },
|
|
127
|
+
] },
|
|
128
|
+
{ name: 'evidencePreservingReducer', fields: [
|
|
129
|
+
{ key: 'enabled', kind: 'bool' },
|
|
130
|
+
{ key: 'logEnabled', kind: 'bool' },
|
|
131
|
+
{ key: 'model', kind: 'optionalString' },
|
|
132
|
+
{ key: 'minBytes', kind: 'int', min: 512 },
|
|
133
|
+
{ key: 'maxChars', kind: 'int', min: 1000 },
|
|
134
|
+
{ key: 'maxOutputTokens', kind: 'int', min: 128 },
|
|
135
|
+
{ key: 'timeoutMs', kind: 'int', min: 500 },
|
|
136
|
+
{ key: 'localOnly', kind: 'bool' },
|
|
137
|
+
] },
|
|
138
|
+
{ name: 'jev', fields: [
|
|
139
|
+
{ key: 'enabled', kind: 'bool' },
|
|
140
|
+
{ key: 'logEnabled', kind: 'bool' },
|
|
141
|
+
{ key: 'baseUrl', kind: 'optionalString' },
|
|
142
|
+
{ key: 'model', kind: 'string' },
|
|
143
|
+
{ key: 'timeoutMs', kind: 'int', min: 500 },
|
|
144
|
+
{ key: 'minConfidence', kind: 'number', min: 0, max: 1 },
|
|
145
|
+
{ key: 'apiKey', kind: 'optionalString' },
|
|
146
|
+
] },
|
|
147
|
+
];
|
|
148
|
+
|
|
149
|
+
function issueFor(section: string, field: FieldSpec): string {
|
|
150
|
+
const path = `${section}.${field.key}`;
|
|
151
|
+
switch (field.kind) {
|
|
152
|
+
case 'bool':
|
|
153
|
+
return `${path} 必须是 boolean`;
|
|
154
|
+
case 'int':
|
|
155
|
+
return `${path} 必须是 >= ${field.min} 的整数`;
|
|
156
|
+
case 'number':
|
|
157
|
+
return field.max === undefined
|
|
158
|
+
? `${path} 必须是 >= ${field.minText ?? field.min} 的数字`
|
|
159
|
+
: `${path} 必须是 ${field.min} 到 ${field.max} 之间的数字`;
|
|
160
|
+
case 'ratio':
|
|
161
|
+
return `${path} 必须是 "auto" 或非负有限数`;
|
|
162
|
+
default:
|
|
163
|
+
return `${path} 必须是非空字符串`;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function applyField(target: Record<string, unknown>, field: FieldSpec, raw: unknown): boolean {
|
|
168
|
+
if (raw === undefined) return true;
|
|
169
|
+
const min = field.min ?? Number.NEGATIVE_INFINITY;
|
|
170
|
+
switch (field.kind) {
|
|
171
|
+
case 'bool':
|
|
172
|
+
if (typeof raw !== 'boolean') return false;
|
|
173
|
+
break;
|
|
174
|
+
case 'int':
|
|
175
|
+
if (typeof raw !== 'number' || !Number.isInteger(raw) || raw < min) return false;
|
|
176
|
+
break;
|
|
177
|
+
case 'number':
|
|
178
|
+
if (typeof raw !== 'number' || !Number.isFinite(raw) || raw < min) return false;
|
|
179
|
+
if (field.max !== undefined && raw > field.max) return false;
|
|
180
|
+
break;
|
|
181
|
+
case 'ratio':
|
|
182
|
+
if (raw !== 'auto' && (typeof raw !== 'number' || !Number.isFinite(raw) || raw < 0)) return false;
|
|
183
|
+
break;
|
|
184
|
+
case 'string':
|
|
185
|
+
if (typeof raw !== 'string' || !raw.trim()) return false;
|
|
186
|
+
target[field.key] = raw.trim();
|
|
187
|
+
return true;
|
|
188
|
+
case 'optionalString':
|
|
189
|
+
if (raw === '' || raw === null) {
|
|
190
|
+
target[field.key] = undefined;
|
|
191
|
+
return true;
|
|
192
|
+
}
|
|
193
|
+
if (typeof raw !== 'string' || !raw.trim()) return false;
|
|
194
|
+
target[field.key] = raw.trim();
|
|
195
|
+
return true;
|
|
196
|
+
}
|
|
197
|
+
target[field.key] = raw;
|
|
198
|
+
return true;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function defaults(): EfficiencyConfig {
|
|
202
|
+
return {
|
|
203
|
+
version: 1,
|
|
204
|
+
onlineContextCompact: { ...DEFAULT_EFFICIENCY_CONFIG.onlineContextCompact },
|
|
205
|
+
observationPack: { ...DEFAULT_EFFICIENCY_CONFIG.observationPack },
|
|
206
|
+
evidencePreservingReducer: { ...DEFAULT_EFFICIENCY_CONFIG.evidencePreservingReducer },
|
|
207
|
+
jev: { ...DEFAULT_EFFICIENCY_CONFIG.jev },
|
|
208
|
+
};
|
|
209
|
+
}
|
|
163
210
|
|
|
164
211
|
export interface ValidateConfigResult {
|
|
165
212
|
ok: boolean;
|
|
@@ -168,301 +215,124 @@ export interface ValidateConfigResult {
|
|
|
168
215
|
}
|
|
169
216
|
|
|
170
217
|
export function validateEfficiencyConfig(raw: unknown): ValidateConfigResult {
|
|
218
|
+
const config = defaults();
|
|
171
219
|
if (!isPlainObject(raw)) {
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
config: {
|
|
175
|
-
version: 1,
|
|
176
|
-
onlineContextCompact: { ...DEFAULT_EFFICIENCY_CONFIG.onlineContextCompact, enabled: false },
|
|
177
|
-
observationPack: { ...DEFAULT_EFFICIENCY_CONFIG.observationPack, enabled: false },
|
|
178
|
-
evidencePreservingReducer: { ...DEFAULT_EFFICIENCY_CONFIG.evidencePreservingReducer, enabled: false },
|
|
179
|
-
jev: { ...DEFAULT_EFFICIENCY_CONFIG.jev, enabled: false },
|
|
180
|
-
},
|
|
181
|
-
issues: ['efficiency config 必须是 JSON 对象'],
|
|
182
|
-
};
|
|
220
|
+
for (const section of SECTIONS) config[section.name].enabled = false;
|
|
221
|
+
return { ok: false, config, issues: ['efficiency config 必须是 JSON 对象'] };
|
|
183
222
|
}
|
|
184
223
|
|
|
185
|
-
const
|
|
186
|
-
const
|
|
187
|
-
|
|
188
|
-
const eprIssues: string[] = [];
|
|
189
|
-
const jevIssues: string[] = [];
|
|
190
|
-
|
|
191
|
-
for (const k of Object.keys(raw)) {
|
|
192
|
-
if (!TOP_LEVEL_KEYS.has(k)) {
|
|
193
|
-
topIssues.push(`未知顶层配置项: "${k}"`);
|
|
194
|
-
}
|
|
224
|
+
const issues: string[] = [];
|
|
225
|
+
for (const key of Object.keys(raw)) {
|
|
226
|
+
if (key !== 'version' && !SECTIONS.some((s) => s.name === key)) issues.push(`未知顶层配置项: "${key}"`);
|
|
195
227
|
}
|
|
196
|
-
|
|
197
228
|
if (raw.version !== undefined && raw.version !== 1) {
|
|
198
|
-
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
// Section 1: onlineContextCompact
|
|
202
|
-
let occ: OnlineContextCompactConfig = { ...DEFAULT_EFFICIENCY_CONFIG.onlineContextCompact };
|
|
203
|
-
if (raw.onlineContextCompact !== undefined) {
|
|
204
|
-
if (!isPlainObject(raw.onlineContextCompact)) {
|
|
205
|
-
occIssues.push('onlineContextCompact 必须是 JSON 对象');
|
|
206
|
-
} else {
|
|
207
|
-
const o = raw.onlineContextCompact;
|
|
208
|
-
for (const k of Object.keys(o)) {
|
|
209
|
-
if (!OCC_KEYS.has(k)) occIssues.push(`onlineContextCompact 未知配置项: "${k}"`);
|
|
210
|
-
}
|
|
211
|
-
if (o.enabled !== undefined) {
|
|
212
|
-
if (typeof o.enabled !== 'boolean') occIssues.push('onlineContextCompact.enabled 必须是 boolean');
|
|
213
|
-
else occ.enabled = o.enabled;
|
|
214
|
-
}
|
|
215
|
-
if (o.logEnabled !== undefined) {
|
|
216
|
-
if (typeof o.logEnabled !== 'boolean') occIssues.push('onlineContextCompact.logEnabled 必须是 boolean');
|
|
217
|
-
else occ.logEnabled = o.logEnabled;
|
|
218
|
-
}
|
|
219
|
-
if (o.cacheWriteReadRatio !== undefined) {
|
|
220
|
-
if (o.cacheWriteReadRatio === 'auto') {
|
|
221
|
-
occ.cacheWriteReadRatio = 'auto';
|
|
222
|
-
} else if (typeof o.cacheWriteReadRatio === 'number' && Number.isFinite(o.cacheWriteReadRatio) && o.cacheWriteReadRatio >= 0) {
|
|
223
|
-
occ.cacheWriteReadRatio = o.cacheWriteReadRatio;
|
|
224
|
-
} else {
|
|
225
|
-
occIssues.push('onlineContextCompact.cacheWriteReadRatio 必须是 "auto" 或非负有限数');
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
if (o.keepRecentTokens !== undefined) {
|
|
229
|
-
if (typeof o.keepRecentTokens === 'number' && Number.isInteger(o.keepRecentTokens) && o.keepRecentTokens >= 1000) {
|
|
230
|
-
occ.keepRecentTokens = o.keepRecentTokens;
|
|
231
|
-
} else {
|
|
232
|
-
occIssues.push('onlineContextCompact.keepRecentTokens 必须是 >= 1000 的整数');
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
if (o.firstCompactionRequestScale !== undefined) {
|
|
236
|
-
if (typeof o.firstCompactionRequestScale === 'number' && Number.isFinite(o.firstCompactionRequestScale) && o.firstCompactionRequestScale >= 1.0) {
|
|
237
|
-
occ.firstCompactionRequestScale = o.firstCompactionRequestScale;
|
|
238
|
-
} else {
|
|
239
|
-
occIssues.push('onlineContextCompact.firstCompactionRequestScale 必须是 >= 1.0 的数字');
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
if (o.subsequentCompactionMargin !== undefined) {
|
|
243
|
-
if (typeof o.subsequentCompactionMargin === 'number' && Number.isFinite(o.subsequentCompactionMargin) && o.subsequentCompactionMargin >= 1.0) {
|
|
244
|
-
occ.subsequentCompactionMargin = o.subsequentCompactionMargin;
|
|
245
|
-
} else {
|
|
246
|
-
occIssues.push('onlineContextCompact.subsequentCompactionMargin 必须是 >= 1.0 的数字');
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
// Section 2: observationPack
|
|
253
|
-
let obs: ObservationPackConfig = { ...DEFAULT_EFFICIENCY_CONFIG.observationPack };
|
|
254
|
-
if (raw.observationPack !== undefined) {
|
|
255
|
-
if (!isPlainObject(raw.observationPack)) {
|
|
256
|
-
obsIssues.push('observationPack 必须是 JSON 对象');
|
|
257
|
-
} else {
|
|
258
|
-
const o = raw.observationPack;
|
|
259
|
-
for (const k of Object.keys(o)) {
|
|
260
|
-
if (!OBS_KEYS.has(k)) obsIssues.push(`observationPack 未知配置项: "${k}"`);
|
|
261
|
-
}
|
|
262
|
-
if (o.enabled !== undefined) {
|
|
263
|
-
if (typeof o.enabled !== 'boolean') obsIssues.push('observationPack.enabled 必须是 boolean');
|
|
264
|
-
else obs.enabled = o.enabled;
|
|
265
|
-
}
|
|
266
|
-
if (o.logEnabled !== undefined) {
|
|
267
|
-
if (typeof o.logEnabled !== 'boolean') obsIssues.push('observationPack.logEnabled 必须是 boolean');
|
|
268
|
-
else obs.logEnabled = o.logEnabled;
|
|
269
|
-
}
|
|
270
|
-
if (o.thresholdBytes !== undefined) {
|
|
271
|
-
if (typeof o.thresholdBytes === 'number' && Number.isInteger(o.thresholdBytes) && o.thresholdBytes >= 1024) {
|
|
272
|
-
obs.thresholdBytes = o.thresholdBytes;
|
|
273
|
-
} else {
|
|
274
|
-
obsIssues.push('observationPack.thresholdBytes 必须是 >= 1024 的整数');
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
if (o.fullSends !== undefined) {
|
|
278
|
-
if (typeof o.fullSends === 'number' && Number.isInteger(o.fullSends) && o.fullSends >= 1) {
|
|
279
|
-
obs.fullSends = o.fullSends;
|
|
280
|
-
} else {
|
|
281
|
-
obsIssues.push('observationPack.fullSends 必须是 >= 1 的整数');
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
if (o.recallChunkBytes !== undefined) {
|
|
285
|
-
if (typeof o.recallChunkBytes === 'number' && Number.isInteger(o.recallChunkBytes) && o.recallChunkBytes >= 1024) {
|
|
286
|
-
obs.recallChunkBytes = o.recallChunkBytes;
|
|
287
|
-
} else {
|
|
288
|
-
obsIssues.push('observationPack.recallChunkBytes 必须是 >= 1024 的整数');
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
if (o.excerptBytes !== undefined) {
|
|
292
|
-
if (typeof o.excerptBytes === 'number' && Number.isInteger(o.excerptBytes) && o.excerptBytes >= 128) {
|
|
293
|
-
obs.excerptBytes = o.excerptBytes;
|
|
294
|
-
} else {
|
|
295
|
-
obsIssues.push('observationPack.excerptBytes 必须是 >= 128 的整数');
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
// Section 3: evidencePreservingReducer
|
|
302
|
-
let epr: EvidencePreservingReducerConfig = { ...DEFAULT_EFFICIENCY_CONFIG.evidencePreservingReducer };
|
|
303
|
-
if (raw.evidencePreservingReducer !== undefined) {
|
|
304
|
-
if (!isPlainObject(raw.evidencePreservingReducer)) {
|
|
305
|
-
eprIssues.push('evidencePreservingReducer 必须是 JSON 对象');
|
|
306
|
-
} else {
|
|
307
|
-
const o = raw.evidencePreservingReducer;
|
|
308
|
-
for (const k of Object.keys(o)) {
|
|
309
|
-
if (!EPR_KEYS.has(k)) eprIssues.push(`evidencePreservingReducer 未知配置项: "${k}"`);
|
|
310
|
-
}
|
|
311
|
-
if (o.enabled !== undefined) {
|
|
312
|
-
if (typeof o.enabled !== 'boolean') eprIssues.push('evidencePreservingReducer.enabled 必须是 boolean');
|
|
313
|
-
else epr.enabled = o.enabled;
|
|
314
|
-
}
|
|
315
|
-
if (o.logEnabled !== undefined) {
|
|
316
|
-
if (typeof o.logEnabled !== 'boolean') eprIssues.push('evidencePreservingReducer.logEnabled 必须是 boolean');
|
|
317
|
-
else epr.logEnabled = o.logEnabled;
|
|
318
|
-
}
|
|
319
|
-
if (o.model !== undefined) {
|
|
320
|
-
if (typeof o.model === 'string' && o.model.trim()) {
|
|
321
|
-
epr.model = o.model.trim();
|
|
322
|
-
} else if (o.model === '' || o.model === null) {
|
|
323
|
-
epr.model = undefined;
|
|
324
|
-
} else {
|
|
325
|
-
eprIssues.push('evidencePreservingReducer.model 必须是非空字符串');
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
if (o.minBytes !== undefined) {
|
|
329
|
-
if (typeof o.minBytes === 'number' && Number.isInteger(o.minBytes) && o.minBytes >= 512) {
|
|
330
|
-
epr.minBytes = o.minBytes;
|
|
331
|
-
} else {
|
|
332
|
-
eprIssues.push('evidencePreservingReducer.minBytes 必须是 >= 512 的整数');
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
if (o.maxChars !== undefined) {
|
|
336
|
-
if (typeof o.maxChars === 'number' && Number.isInteger(o.maxChars) && o.maxChars >= 1000) {
|
|
337
|
-
epr.maxChars = o.maxChars;
|
|
338
|
-
} else {
|
|
339
|
-
eprIssues.push('evidencePreservingReducer.maxChars 必须是 >= 1000 的整数');
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
if (o.maxOutputTokens !== undefined) {
|
|
343
|
-
if (typeof o.maxOutputTokens === 'number' && Number.isInteger(o.maxOutputTokens) && o.maxOutputTokens >= 128) {
|
|
344
|
-
epr.maxOutputTokens = o.maxOutputTokens;
|
|
345
|
-
} else {
|
|
346
|
-
eprIssues.push('evidencePreservingReducer.maxOutputTokens 必须是 >= 128 的整数');
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
if (o.timeoutMs !== undefined) {
|
|
350
|
-
if (typeof o.timeoutMs === 'number' && Number.isInteger(o.timeoutMs) && o.timeoutMs >= 500) {
|
|
351
|
-
epr.timeoutMs = o.timeoutMs;
|
|
352
|
-
} else {
|
|
353
|
-
eprIssues.push('evidencePreservingReducer.timeoutMs 必须是 >= 500 的整数');
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
if (o.localOnly !== undefined) {
|
|
357
|
-
if (typeof o.localOnly !== 'boolean') eprIssues.push('evidencePreservingReducer.localOnly 必须是 boolean');
|
|
358
|
-
else epr.localOnly = o.localOnly;
|
|
359
|
-
}
|
|
360
|
-
}
|
|
229
|
+
issues.push(`version 必须是 1 (当前: ${JSON.stringify(raw.version)})`);
|
|
361
230
|
}
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
if (typeof o.enabled !== 'boolean') jevIssues.push('jev.enabled 必须是 boolean');
|
|
375
|
-
else jev.enabled = o.enabled;
|
|
376
|
-
}
|
|
377
|
-
if (o.logEnabled !== undefined) {
|
|
378
|
-
if (typeof o.logEnabled !== 'boolean') jevIssues.push('jev.logEnabled 必须是 boolean');
|
|
379
|
-
else jev.logEnabled = o.logEnabled;
|
|
380
|
-
}
|
|
381
|
-
if (o.baseUrl !== undefined) {
|
|
382
|
-
if (typeof o.baseUrl === 'string' && o.baseUrl.trim()) {
|
|
383
|
-
jev.baseUrl = o.baseUrl.trim();
|
|
384
|
-
} else if (o.baseUrl === '' || o.baseUrl === null) {
|
|
385
|
-
jev.baseUrl = undefined;
|
|
386
|
-
} else {
|
|
387
|
-
jevIssues.push('jev.baseUrl 必须是非空字符串');
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
if (o.model !== undefined) {
|
|
391
|
-
if (typeof o.model === 'string' && o.model.trim()) {
|
|
392
|
-
jev.model = o.model.trim();
|
|
393
|
-
} else {
|
|
394
|
-
jevIssues.push('jev.model 必须是非空字符串');
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
if (o.timeoutMs !== undefined) {
|
|
398
|
-
if (typeof o.timeoutMs === 'number' && Number.isInteger(o.timeoutMs) && o.timeoutMs >= 500) {
|
|
399
|
-
jev.timeoutMs = o.timeoutMs;
|
|
400
|
-
} else {
|
|
401
|
-
jevIssues.push('jev.timeoutMs 必须是 >= 500 的整数');
|
|
231
|
+
const topLevelInvalid = issues.length > 0;
|
|
232
|
+
|
|
233
|
+
for (const section of SECTIONS) {
|
|
234
|
+
const issueCountBefore = issues.length;
|
|
235
|
+
const target = config[section.name] as unknown as Record<string, unknown>;
|
|
236
|
+
const rawSection = raw[section.name];
|
|
237
|
+
if (rawSection !== undefined) {
|
|
238
|
+
if (!isPlainObject(rawSection)) {
|
|
239
|
+
issues.push(`${section.name} 必须是 JSON 对象`);
|
|
240
|
+
} else {
|
|
241
|
+
for (const key of Object.keys(rawSection)) {
|
|
242
|
+
if (!section.fields.some((f) => f.key === key)) issues.push(`${section.name} 未知配置项: "${key}"`);
|
|
402
243
|
}
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
if (typeof o.minConfidence === 'number' && Number.isFinite(o.minConfidence) && o.minConfidence >= 0 && o.minConfidence <= 1) {
|
|
406
|
-
jev.minConfidence = o.minConfidence;
|
|
407
|
-
} else {
|
|
408
|
-
jevIssues.push('jev.minConfidence 必须是 0 到 1 之间的数字');
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
if (o.apiKey !== undefined) {
|
|
412
|
-
if (typeof o.apiKey === 'string' && o.apiKey.trim()) {
|
|
413
|
-
jev.apiKey = o.apiKey.trim();
|
|
414
|
-
} else if (o.apiKey === '' || o.apiKey === null) {
|
|
415
|
-
jev.apiKey = undefined;
|
|
416
|
-
} else {
|
|
417
|
-
jevIssues.push('jev.apiKey 必须是非空字符串');
|
|
244
|
+
for (const field of section.fields) {
|
|
245
|
+
if (!applyField(target, field, rawSection[field.key])) issues.push(issueFor(section.name, field));
|
|
418
246
|
}
|
|
419
247
|
}
|
|
420
248
|
}
|
|
249
|
+
// Fail-open: an invalid section (or an invalid top level) can never enable a mechanism.
|
|
250
|
+
if (topLevelInvalid || issues.length > issueCountBefore) config[section.name].enabled = false;
|
|
421
251
|
}
|
|
422
252
|
|
|
423
|
-
|
|
424
|
-
if (topIssues.length > 0) {
|
|
425
|
-
occ.enabled = false;
|
|
426
|
-
obs.enabled = false;
|
|
427
|
-
epr.enabled = false;
|
|
428
|
-
jev.enabled = false;
|
|
429
|
-
}
|
|
430
|
-
if (occIssues.length > 0) {
|
|
431
|
-
occ.enabled = false;
|
|
432
|
-
}
|
|
433
|
-
if (obsIssues.length > 0) {
|
|
434
|
-
obs.enabled = false;
|
|
435
|
-
}
|
|
436
|
-
if (eprIssues.length > 0) {
|
|
437
|
-
epr.enabled = false;
|
|
438
|
-
}
|
|
439
|
-
if (jevIssues.length > 0) {
|
|
440
|
-
jev.enabled = false;
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
const issues = [...topIssues, ...occIssues, ...obsIssues, ...eprIssues, ...jevIssues];
|
|
444
|
-
|
|
445
|
-
return {
|
|
446
|
-
ok: issues.length === 0,
|
|
447
|
-
config: {
|
|
448
|
-
version: 1,
|
|
449
|
-
onlineContextCompact: occ,
|
|
450
|
-
observationPack: obs,
|
|
451
|
-
evidencePreservingReducer: epr,
|
|
452
|
-
jev,
|
|
453
|
-
},
|
|
454
|
-
issues,
|
|
455
|
-
};
|
|
253
|
+
return { ok: issues.length === 0, config, issues };
|
|
456
254
|
}
|
|
457
255
|
|
|
458
|
-
function parseEnvBool(val: string
|
|
459
|
-
if (!val) return undefined;
|
|
256
|
+
function parseEnvBool(val: string): boolean | undefined {
|
|
460
257
|
const s = val.trim().toLowerCase();
|
|
461
258
|
if (s === '1' || s === 'true' || s === 'on') return true;
|
|
462
259
|
if (s === '0' || s === 'false' || s === 'off') return false;
|
|
463
260
|
return undefined;
|
|
464
261
|
}
|
|
465
262
|
|
|
263
|
+
interface EnvOverride {
|
|
264
|
+
readonly env: string;
|
|
265
|
+
readonly section: SectionSpec['name'];
|
|
266
|
+
readonly key: string;
|
|
267
|
+
readonly kind: 'bool' | 'string' | 'ratio' | 'int' | 'unit';
|
|
268
|
+
readonly min?: number;
|
|
269
|
+
/** Set only when the config left the key unset (foreign-convention fallback). */
|
|
270
|
+
readonly fallbackOnly?: boolean;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/** Highest-precedence layer; an empty value counts as unset everywhere. */
|
|
274
|
+
const ENV_OVERRIDES: readonly EnvOverride[] = [
|
|
275
|
+
{ env: 'PI_HERDR_COMPACT_ENABLE', section: 'onlineContextCompact', key: 'enabled', kind: 'bool' },
|
|
276
|
+
{ env: 'PI_HERDR_COMPACT_LOG', section: 'onlineContextCompact', key: 'logEnabled', kind: 'bool' },
|
|
277
|
+
{ env: 'PI_HERDR_CACHE_RATIO', section: 'onlineContextCompact', key: 'cacheWriteReadRatio', kind: 'ratio' },
|
|
278
|
+
{ env: 'PI_HERDR_OBS_PACK_ENABLE', section: 'observationPack', key: 'enabled', kind: 'bool' },
|
|
279
|
+
{ env: 'PI_HERDR_OBS_PACK_LOG', section: 'observationPack', key: 'logEnabled', kind: 'bool' },
|
|
280
|
+
{ env: 'PI_HERDR_REDUCER_ENABLE', section: 'evidencePreservingReducer', key: 'enabled', kind: 'bool' },
|
|
281
|
+
{ env: 'PI_HERDR_REDUCER_LOG', section: 'evidencePreservingReducer', key: 'logEnabled', kind: 'bool' },
|
|
282
|
+
{ env: 'PI_HERDR_REDUCER_MODEL', section: 'evidencePreservingReducer', key: 'model', kind: 'string' },
|
|
283
|
+
// jev has no legacy spelling: PIER_JEV_* is canonical from the start.
|
|
284
|
+
{ env: 'PIER_JEV_ENABLE', section: 'jev', key: 'enabled', kind: 'bool' },
|
|
285
|
+
{ env: 'PIER_JEV_LOG', section: 'jev', key: 'logEnabled', kind: 'bool' },
|
|
286
|
+
{ env: 'PIER_JEV_MODEL', section: 'jev', key: 'model', kind: 'string' },
|
|
287
|
+
{ env: 'PIER_JEV_BASE_URL', section: 'jev', key: 'baseUrl', kind: 'string' },
|
|
288
|
+
{ env: 'PIER_JEV_TIMEOUT_MS', section: 'jev', key: 'timeoutMs', kind: 'int', min: 500 },
|
|
289
|
+
{ env: 'PIER_JEV_MIN_CONFIDENCE', section: 'jev', key: 'minConfidence', kind: 'unit' },
|
|
290
|
+
{ env: 'PIER_JEV_API_KEY', section: 'jev', key: 'apiKey', kind: 'string' },
|
|
291
|
+
// TYPESAFE_API_KEY is a foreign (SDK) convention, so it ranks below the config file too.
|
|
292
|
+
{ env: 'TYPESAFE_API_KEY', section: 'jev', key: 'apiKey', kind: 'string', fallbackOnly: true },
|
|
293
|
+
];
|
|
294
|
+
|
|
295
|
+
function applyEnvOverrides(
|
|
296
|
+
config: EfficiencyConfig,
|
|
297
|
+
env: Record<string, string | undefined>,
|
|
298
|
+
warn: (message: string) => void,
|
|
299
|
+
): void {
|
|
300
|
+
for (const override of ENV_OVERRIDES) {
|
|
301
|
+
const raw = env[override.env];
|
|
302
|
+
if (raw === undefined || raw === '') continue;
|
|
303
|
+
const target = config[override.section] as unknown as Record<string, unknown>;
|
|
304
|
+
if (override.fallbackOnly && target[override.key] !== undefined) continue;
|
|
305
|
+
|
|
306
|
+
if (override.kind === 'bool') {
|
|
307
|
+
const parsed = parseEnvBool(raw);
|
|
308
|
+
if (parsed !== undefined) target[override.key] = parsed;
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
if (override.kind === 'string') {
|
|
312
|
+
if (raw.trim()) target[override.key] = raw.trim();
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
if (override.kind === 'ratio' && raw.trim().toLowerCase() === 'auto') {
|
|
316
|
+
target[override.key] = 'auto';
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
const num = Number(raw);
|
|
320
|
+
const valid =
|
|
321
|
+
override.kind === 'ratio' ? Number.isFinite(num) && num >= 0
|
|
322
|
+
: override.kind === 'unit' ? Number.isFinite(num) && num >= 0 && num <= 1
|
|
323
|
+
: Number.isInteger(num) && num >= (override.min ?? 0);
|
|
324
|
+
if (valid) {
|
|
325
|
+
target[override.key] = num;
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
const requirement =
|
|
329
|
+
override.kind === 'unit' ? '(需 0 到 1)'
|
|
330
|
+
: override.kind === 'int' ? `(需 >= ${override.min} 的整数)`
|
|
331
|
+
: '';
|
|
332
|
+
warn(`无效环境变量 ${override.env}="${raw}"${requirement},忽略`);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
466
336
|
export interface ResolveConfigOptions {
|
|
467
337
|
workspaceConfig?: unknown;
|
|
468
338
|
userConfig?: unknown;
|
|
@@ -473,128 +343,28 @@ export interface ResolveConfigOptions {
|
|
|
473
343
|
|
|
474
344
|
export function resolveEfficiencyConfig(opts: ResolveConfigOptions = {}): EfficiencyConfig {
|
|
475
345
|
const env = opts.env ?? process.env;
|
|
476
|
-
const warn = opts.onWarning ?? ((msg) => console.warn(`[pi-herdr] efficiency config warning: ${msg}`));
|
|
346
|
+
const warn = opts.onWarning ?? ((msg: string) => console.warn(`[pi-herdr] efficiency config warning: ${msg}`));
|
|
477
347
|
const isTrusted = opts.isProjectTrusted ?? false;
|
|
478
348
|
|
|
479
|
-
|
|
480
|
-
|
|
349
|
+
// An untrusted project's file is ignored wholesale (never merged with the user layer).
|
|
350
|
+
let baseRaw: unknown = opts.userConfig;
|
|
481
351
|
if (opts.workspaceConfig !== undefined) {
|
|
482
|
-
if (isTrusted)
|
|
483
|
-
|
|
484
|
-
} else {
|
|
485
|
-
warn('工作区能效配置未被信任(isProjectTrusted=false),已安全忽略工作区配置');
|
|
486
|
-
baseRaw = opts.userConfig;
|
|
487
|
-
}
|
|
488
|
-
} else {
|
|
489
|
-
baseRaw = opts.userConfig;
|
|
352
|
+
if (isTrusted) baseRaw = opts.workspaceConfig;
|
|
353
|
+
else warn('工作区能效配置未被信任(isProjectTrusted=false),已安全忽略工作区配置');
|
|
490
354
|
}
|
|
491
355
|
|
|
492
|
-
let resolved
|
|
356
|
+
let resolved = defaults();
|
|
493
357
|
if (baseRaw !== undefined) {
|
|
494
358
|
const validated = validateEfficiencyConfig(baseRaw);
|
|
495
|
-
if (!validated.ok) {
|
|
496
|
-
warn(`配置存在校验问题,已安全回退默认值: ${validated.issues.join('; ')}`);
|
|
497
|
-
}
|
|
359
|
+
if (!validated.ok) warn(`配置存在校验问题,已安全回退默认值: ${validated.issues.join('; ')}`);
|
|
498
360
|
resolved = validated.config;
|
|
499
|
-
} else {
|
|
500
|
-
resolved = {
|
|
501
|
-
version: 1,
|
|
502
|
-
onlineContextCompact: { ...DEFAULT_EFFICIENCY_CONFIG.onlineContextCompact },
|
|
503
|
-
observationPack: { ...DEFAULT_EFFICIENCY_CONFIG.observationPack },
|
|
504
|
-
evidencePreservingReducer: { ...DEFAULT_EFFICIENCY_CONFIG.evidencePreservingReducer },
|
|
505
|
-
jev: { ...DEFAULT_EFFICIENCY_CONFIG.jev },
|
|
506
|
-
};
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
// Apply environment variable overrides (highest precedence)
|
|
510
|
-
const occEnabled = parseEnvBool(env.PI_HERDR_COMPACT_ENABLE);
|
|
511
|
-
if (occEnabled !== undefined) resolved.onlineContextCompact.enabled = occEnabled;
|
|
512
|
-
|
|
513
|
-
const occLog = parseEnvBool(env.PI_HERDR_COMPACT_LOG);
|
|
514
|
-
if (occLog !== undefined) resolved.onlineContextCompact.logEnabled = occLog;
|
|
515
|
-
|
|
516
|
-
const cacheRatioRaw = env.PI_HERDR_CACHE_RATIO;
|
|
517
|
-
if (cacheRatioRaw !== undefined) {
|
|
518
|
-
if (cacheRatioRaw.trim().toLowerCase() === 'auto') {
|
|
519
|
-
resolved.onlineContextCompact.cacheWriteReadRatio = 'auto';
|
|
520
|
-
} else {
|
|
521
|
-
const num = Number(cacheRatioRaw);
|
|
522
|
-
if (Number.isFinite(num) && num >= 0) {
|
|
523
|
-
resolved.onlineContextCompact.cacheWriteReadRatio = num;
|
|
524
|
-
} else {
|
|
525
|
-
warn(`无效环境变量 PI_HERDR_CACHE_RATIO="${cacheRatioRaw}",忽略`);
|
|
526
|
-
}
|
|
527
|
-
}
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
const obsEnabled = parseEnvBool(env.PI_HERDR_OBS_PACK_ENABLE);
|
|
531
|
-
if (obsEnabled !== undefined) resolved.observationPack.enabled = obsEnabled;
|
|
532
|
-
|
|
533
|
-
const obsLog = parseEnvBool(env.PI_HERDR_OBS_PACK_LOG);
|
|
534
|
-
if (obsLog !== undefined) resolved.observationPack.logEnabled = obsLog;
|
|
535
|
-
|
|
536
|
-
const redEnabled = parseEnvBool(env.PI_HERDR_REDUCER_ENABLE);
|
|
537
|
-
if (redEnabled !== undefined) resolved.evidencePreservingReducer.enabled = redEnabled;
|
|
538
|
-
|
|
539
|
-
const redLog = parseEnvBool(env.PI_HERDR_REDUCER_LOG);
|
|
540
|
-
if (redLog !== undefined) resolved.evidencePreservingReducer.logEnabled = redLog;
|
|
541
|
-
|
|
542
|
-
const redModel = env.PI_HERDR_REDUCER_MODEL;
|
|
543
|
-
if (redModel !== undefined && redModel.trim()) {
|
|
544
|
-
resolved.evidencePreservingReducer.model = redModel.trim();
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
// jev decision layer (new mechanism: canonical PIER_JEV_* only, no legacy spelling exists).
|
|
548
|
-
// TYPESAFE_API_KEY is a foreign-convention fallback (TypeSafe SDK reads it), so it ranks
|
|
549
|
-
// below pier's own env and below an explicit config-file key.
|
|
550
|
-
const jevEnabled = parseEnvBool(env.PIER_JEV_ENABLE);
|
|
551
|
-
if (jevEnabled !== undefined) resolved.jev.enabled = jevEnabled;
|
|
552
|
-
|
|
553
|
-
const jevLog = parseEnvBool(env.PIER_JEV_LOG);
|
|
554
|
-
if (jevLog !== undefined) resolved.jev.logEnabled = jevLog;
|
|
555
|
-
|
|
556
|
-
const jevModel = env.PIER_JEV_MODEL;
|
|
557
|
-
if (jevModel !== undefined && jevModel.trim()) {
|
|
558
|
-
resolved.jev.model = jevModel.trim();
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
const jevBase = env.PIER_JEV_BASE_URL;
|
|
562
|
-
if (jevBase !== undefined && jevBase.trim()) {
|
|
563
|
-
resolved.jev.baseUrl = jevBase.trim();
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
const jevTimeoutRaw = env.PIER_JEV_TIMEOUT_MS;
|
|
567
|
-
if (jevTimeoutRaw !== undefined) {
|
|
568
|
-
const num = Number(jevTimeoutRaw);
|
|
569
|
-
if (Number.isInteger(num) && num >= 500) {
|
|
570
|
-
resolved.jev.timeoutMs = num;
|
|
571
|
-
} else {
|
|
572
|
-
warn(`无效环境变量 PIER_JEV_TIMEOUT_MS="${jevTimeoutRaw}"(需 >= 500 的整数),忽略`);
|
|
573
|
-
}
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
const jevMinConfRaw = env.PIER_JEV_MIN_CONFIDENCE;
|
|
577
|
-
if (jevMinConfRaw !== undefined) {
|
|
578
|
-
const num = Number(jevMinConfRaw);
|
|
579
|
-
if (Number.isFinite(num) && num >= 0 && num <= 1) {
|
|
580
|
-
resolved.jev.minConfidence = num;
|
|
581
|
-
} else {
|
|
582
|
-
warn(`无效环境变量 PIER_JEV_MIN_CONFIDENCE="${jevMinConfRaw}"(需 0 到 1),忽略`);
|
|
583
|
-
}
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
const jevKey = env.PIER_JEV_API_KEY;
|
|
587
|
-
if (jevKey !== undefined && jevKey.trim()) {
|
|
588
|
-
resolved.jev.apiKey = jevKey.trim();
|
|
589
|
-
}
|
|
590
|
-
if (!resolved.jev.apiKey && env.TYPESAFE_API_KEY && env.TYPESAFE_API_KEY.trim()) {
|
|
591
|
-
resolved.jev.apiKey = env.TYPESAFE_API_KEY.trim();
|
|
592
361
|
}
|
|
593
362
|
|
|
363
|
+
applyEnvOverrides(resolved, env, warn);
|
|
594
364
|
return resolved;
|
|
595
365
|
}
|
|
596
366
|
|
|
597
|
-
|
|
367
|
+
function defaultUserEfficiencyConfigDir(): string {
|
|
598
368
|
return join(homedir(), '.pi', 'agent', 'herdr-pi');
|
|
599
369
|
}
|
|
600
370
|
|
|
@@ -606,6 +376,44 @@ export function defaultUserEfficiencyConfigFile(): string {
|
|
|
606
376
|
return join(defaultUserEfficiencyConfigDir(), 'config.json');
|
|
607
377
|
}
|
|
608
378
|
|
|
379
|
+
interface PiCompactionSettings {
|
|
380
|
+
enabled?: boolean;
|
|
381
|
+
keepRecentTokens?: number;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function readCompactionSettings(filePath: string): PiCompactionSettings {
|
|
385
|
+
const parsed = readJsonConfig(filePath);
|
|
386
|
+
if (!isPlainObject(parsed) || !isPlainObject(parsed.compaction)) return {};
|
|
387
|
+
const { compaction } = parsed;
|
|
388
|
+
const result: PiCompactionSettings = {};
|
|
389
|
+
if (typeof compaction.enabled === 'boolean') result.enabled = compaction.enabled;
|
|
390
|
+
if (typeof compaction.keepRecentTokens === 'number') result.keepRecentTokens = compaction.keepRecentTokens;
|
|
391
|
+
return result;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function readJsonConfig(filePath: string, onError?: (error: unknown) => void): unknown {
|
|
395
|
+
if (!existsSync(filePath)) return undefined;
|
|
396
|
+
try {
|
|
397
|
+
return JSON.parse(readFileSync(filePath, 'utf8')) as unknown;
|
|
398
|
+
} catch (error) {
|
|
399
|
+
onError?.(error);
|
|
400
|
+
return undefined;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/** Pi's own compaction settings: global, plus the project file when the project is trusted. */
|
|
405
|
+
export function loadPiNativeCompactionSettings(opts: {
|
|
406
|
+
cwd?: string;
|
|
407
|
+
isProjectTrusted?: boolean;
|
|
408
|
+
agentDir?: string;
|
|
409
|
+
} = {}): PiCompactionSettings {
|
|
410
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
411
|
+
const agentDir = opts.agentDir ?? process.env.PI_CODING_AGENT_DIR ?? join(homedir(), '.pi', 'agent');
|
|
412
|
+
const globalSettings = readCompactionSettings(join(agentDir, 'settings.json'));
|
|
413
|
+
if (!opts.isProjectTrusted) return globalSettings;
|
|
414
|
+
return { ...globalSettings, ...readCompactionSettings(join(cwd, '.pi', 'settings.json')) };
|
|
415
|
+
}
|
|
416
|
+
|
|
609
417
|
export function loadEfficiencyConfigFromDisk(opts: {
|
|
610
418
|
cwd?: string;
|
|
611
419
|
isProjectTrusted?: boolean;
|
|
@@ -613,13 +421,11 @@ export function loadEfficiencyConfigFromDisk(opts: {
|
|
|
613
421
|
onWarning?: (msg: string) => void;
|
|
614
422
|
/** Override the Pi agent dir used to read native `settings.json` (tests/isolation). */
|
|
615
423
|
agentDir?: string;
|
|
616
|
-
/** Override the user-level efficiency config file path (tests/isolation). */
|
|
617
424
|
userConfigPath?: string;
|
|
618
425
|
} = {}): EfficiencyConfig {
|
|
619
426
|
const cwd = opts.cwd ?? process.cwd();
|
|
620
427
|
const wsPath = defaultWorkspaceEfficiencyConfigFile(cwd);
|
|
621
428
|
const userPath = opts.userConfigPath ?? defaultUserEfficiencyConfigFile();
|
|
622
|
-
|
|
623
429
|
const warnFile = (label: string, filePath: string, error: unknown): void => {
|
|
624
430
|
opts.onWarning?.(`${label} ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
625
431
|
};
|
|
@@ -639,23 +445,20 @@ export function loadEfficiencyConfigFromDisk(opts: {
|
|
|
639
445
|
agentDir: opts.agentDir,
|
|
640
446
|
});
|
|
641
447
|
|
|
642
|
-
//
|
|
448
|
+
// pi's global compaction switch wins over an explicitly enabled OCC — only an env force overrides it.
|
|
643
449
|
const env = opts.env ?? process.env;
|
|
644
450
|
if (piSettings.enabled === false && !env.PI_HERDR_COMPACT_ENABLE) {
|
|
645
451
|
resolved.onlineContextCompact.enabled = false;
|
|
646
452
|
}
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
453
|
+
const hasExplicitKeepRecent = [opts.isProjectTrusted ? workspaceConfig : undefined, userConfig].some(
|
|
454
|
+
(config) =>
|
|
455
|
+
isPlainObject(config)
|
|
456
|
+
&& isPlainObject(config.onlineContextCompact)
|
|
457
|
+
&& config.onlineContextCompact.keepRecentTokens !== undefined,
|
|
458
|
+
);
|
|
459
|
+
// pi's keepRecentTokens is inherited only while the efficiency config does not set its own.
|
|
655
460
|
if (!hasExplicitKeepRecent && typeof piSettings.keepRecentTokens === 'number') {
|
|
656
461
|
resolved.onlineContextCompact.keepRecentTokens = piSettings.keepRecentTokens;
|
|
657
462
|
}
|
|
658
|
-
|
|
659
463
|
return resolved;
|
|
660
464
|
}
|
|
661
|
-
|