pi-pier 0.3.0 → 0.4.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 +16 -12
- package/package.json +3 -3
- 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 +10 -13
- package/src/compact-coordinator.ts +279 -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/cordis-services.ts +64 -0
- package/src/dashboard-command.ts +77 -33
- package/src/delegation-core.ts +115 -0
- package/src/efficiency-config-core.ts +286 -486
- package/src/efficiency-store.ts +16 -29
- package/src/gc-core.ts +10 -36
- package/src/guards.ts +6 -0
- package/src/herdr-client.ts +202 -233
- package/src/history-store.ts +38 -23
- package/src/index-gates.ts +118 -0
- package/src/index-locks.ts +2 -7
- package/src/index-master.ts +31 -29
- package/src/index-notices.ts +28 -15
- package/src/index-pipe.ts +31 -12
- package/src/index-roles.ts +306 -0
- package/src/index-runtime.ts +16 -6
- package/src/index-worker.ts +8 -18
- package/src/index.ts +561 -570
- package/src/jev-client.ts +12 -17
- package/src/jev-core.ts +138 -90
- 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/panel-open.ts +47 -0
- package/src/pi-surface.ts +34 -22
- package/src/pier-options.ts +53 -50
- package/src/pipe-channel.ts +44 -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 +766 -0
- package/src/{core → plugins}/terminal.ts +198 -139
- package/src/{core → plugins}/todo.ts +73 -134
- package/src/progress-core.ts +16 -53
- package/src/reconcile-core.ts +24 -33
- package/src/reducer-core.ts +26 -64
- package/src/reducer-invoker.ts +266 -239
- package/src/renderers.ts +108 -84
- package/src/role-loader.ts +33 -28
- package/src/role-manifest.ts +25 -31
- package/src/role-state.ts +119 -0
- package/src/roles/worker-default.json +3 -3
- 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 +61 -22
- 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 +423 -120
- package/src/subagent-gc.ts +109 -67
- package/src/subagent-poller.ts +383 -24
- package/src/subagent-session.ts +516 -0
- package/src/subagent-spawn.ts +577 -52
- package/src/swallow.ts +7 -17
- package/src/terminal-core.ts +187 -134
- package/src/todo-core.ts +57 -109
- package/src/todo-read-hook.ts +46 -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/tree-view.ts +162 -0
- package/src/vocab.ts +52 -25
- package/src/worker-marker.ts +76 -0
- 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/focus-poller.ts +0 -244
- 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,18 @@
|
|
|
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 { isPlainObject } from './guards.ts';
|
|
13
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
16
14
|
import { homedir } from 'node:os';
|
|
17
15
|
import { join } from 'node:path';
|
|
18
|
-
import { loadPiNativeCompactionSettings, readJsonConfig } from './efficiency-config-io.ts';
|
|
19
16
|
|
|
20
17
|
export interface OnlineContextCompactConfig {
|
|
21
18
|
enabled: boolean;
|
|
@@ -47,9 +44,8 @@ export interface EvidencePreservingReducerConfig {
|
|
|
47
44
|
}
|
|
48
45
|
|
|
49
46
|
/**
|
|
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.
|
|
47
|
+
* Jev decision layer (RFC docs/rfc-jev-integration.md): optional System One classification calls that
|
|
48
|
+
* upgrade hand-written heuristics; every call site fails open on any error, timeout, or low confidence.
|
|
53
49
|
*/
|
|
54
50
|
export interface JevConfig {
|
|
55
51
|
enabled: boolean;
|
|
@@ -77,89 +73,137 @@ export interface EfficiencyConfig {
|
|
|
77
73
|
export const DEFAULT_EFFICIENCY_CONFIG: EfficiencyConfig = Object.freeze({
|
|
78
74
|
version: 1,
|
|
79
75
|
onlineContextCompact: Object.freeze({
|
|
80
|
-
enabled: false,
|
|
81
|
-
|
|
82
|
-
cacheWriteReadRatio: 'auto',
|
|
83
|
-
keepRecentTokens: 20000,
|
|
84
|
-
firstCompactionRequestScale: 2.0,
|
|
85
|
-
subsequentCompactionMargin: 1.5,
|
|
76
|
+
enabled: false, logEnabled: false, cacheWriteReadRatio: 'auto', keepRecentTokens: 20000,
|
|
77
|
+
firstCompactionRequestScale: 2.0, subsequentCompactionMargin: 1.5,
|
|
86
78
|
}),
|
|
87
79
|
observationPack: Object.freeze({
|
|
88
|
-
enabled: false,
|
|
89
|
-
|
|
90
|
-
thresholdBytes: 10240,
|
|
91
|
-
fullSends: 2,
|
|
92
|
-
recallChunkBytes: 16384,
|
|
93
|
-
excerptBytes: 1024,
|
|
80
|
+
enabled: false, logEnabled: false, thresholdBytes: 10240, fullSends: 2,
|
|
81
|
+
recallChunkBytes: 16384, excerptBytes: 1024,
|
|
94
82
|
}),
|
|
95
83
|
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,
|
|
84
|
+
enabled: false, logEnabled: false, minBytes: 4096, maxChars: 600000,
|
|
85
|
+
maxOutputTokens: 2048, timeoutMs: 5000, localOnly: false,
|
|
110
86
|
}),
|
|
87
|
+
jev: Object.freeze({ enabled: false, logEnabled: false, model: 'jev-1.13.0', timeoutMs: 2000, minConfidence: 0.6 }),
|
|
111
88
|
});
|
|
112
89
|
|
|
113
|
-
|
|
114
|
-
|
|
90
|
+
type FieldKind = 'bool' | 'int' | 'number' | 'ratio' | 'string' | 'optionalString';
|
|
91
|
+
|
|
92
|
+
interface FieldSpec {
|
|
93
|
+
readonly key: string;
|
|
94
|
+
readonly kind: FieldKind;
|
|
95
|
+
/** Lower bound for `int`/`number`; also printed in the issue message. */
|
|
96
|
+
readonly min?: number;
|
|
97
|
+
readonly max?: number;
|
|
98
|
+
/** Issue text for a bound that must not be printed as a plain JS number ("1.0"). */
|
|
99
|
+
readonly minText?: string;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** A section is `{enabled, ...fields}`; field order fixes the order of reported issues. */
|
|
103
|
+
interface SectionSpec {
|
|
104
|
+
readonly name: 'onlineContextCompact' | 'observationPack' | 'evidencePreservingReducer' | 'jev';
|
|
105
|
+
readonly fields: readonly FieldSpec[];
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const SECTIONS: readonly SectionSpec[] = [
|
|
109
|
+
{ name: 'onlineContextCompact', fields: [
|
|
110
|
+
{ key: 'enabled', kind: 'bool' },
|
|
111
|
+
{ key: 'logEnabled', kind: 'bool' },
|
|
112
|
+
{ key: 'cacheWriteReadRatio', kind: 'ratio' },
|
|
113
|
+
{ key: 'keepRecentTokens', kind: 'int', min: 1000 },
|
|
114
|
+
{ key: 'firstCompactionRequestScale', kind: 'number', min: 1, minText: '1.0' },
|
|
115
|
+
{ key: 'subsequentCompactionMargin', kind: 'number', min: 1, minText: '1.0' },
|
|
116
|
+
] },
|
|
117
|
+
{ name: 'observationPack', fields: [
|
|
118
|
+
{ key: 'enabled', kind: 'bool' },
|
|
119
|
+
{ key: 'logEnabled', kind: 'bool' },
|
|
120
|
+
{ key: 'thresholdBytes', kind: 'int', min: 1024 },
|
|
121
|
+
{ key: 'fullSends', kind: 'int', min: 1 },
|
|
122
|
+
{ key: 'recallChunkBytes', kind: 'int', min: 1024 },
|
|
123
|
+
{ key: 'excerptBytes', kind: 'int', min: 128 },
|
|
124
|
+
] },
|
|
125
|
+
{ name: 'evidencePreservingReducer', fields: [
|
|
126
|
+
{ key: 'enabled', kind: 'bool' },
|
|
127
|
+
{ key: 'logEnabled', kind: 'bool' },
|
|
128
|
+
{ key: 'model', kind: 'optionalString' },
|
|
129
|
+
{ key: 'minBytes', kind: 'int', min: 512 },
|
|
130
|
+
{ key: 'maxChars', kind: 'int', min: 1000 },
|
|
131
|
+
{ key: 'maxOutputTokens', kind: 'int', min: 128 },
|
|
132
|
+
{ key: 'timeoutMs', kind: 'int', min: 500 },
|
|
133
|
+
{ key: 'localOnly', kind: 'bool' },
|
|
134
|
+
] },
|
|
135
|
+
{ name: 'jev', fields: [
|
|
136
|
+
{ key: 'enabled', kind: 'bool' },
|
|
137
|
+
{ key: 'logEnabled', kind: 'bool' },
|
|
138
|
+
{ key: 'baseUrl', kind: 'optionalString' },
|
|
139
|
+
{ key: 'model', kind: 'string' },
|
|
140
|
+
{ key: 'timeoutMs', kind: 'int', min: 500 },
|
|
141
|
+
{ key: 'minConfidence', kind: 'number', min: 0, max: 1 },
|
|
142
|
+
{ key: 'apiKey', kind: 'optionalString' },
|
|
143
|
+
] },
|
|
144
|
+
];
|
|
145
|
+
|
|
146
|
+
function issueFor(section: string, field: FieldSpec): string {
|
|
147
|
+
const path = `${section}.${field.key}`;
|
|
148
|
+
switch (field.kind) {
|
|
149
|
+
case 'bool':
|
|
150
|
+
return `${path} 必须是 boolean`;
|
|
151
|
+
case 'int':
|
|
152
|
+
return `${path} 必须是 >= ${field.min} 的整数`;
|
|
153
|
+
case 'number':
|
|
154
|
+
return field.max === undefined
|
|
155
|
+
? `${path} 必须是 >= ${field.minText ?? field.min} 的数字`
|
|
156
|
+
: `${path} 必须是 ${field.min} 到 ${field.max} 之间的数字`;
|
|
157
|
+
case 'ratio':
|
|
158
|
+
return `${path} 必须是 "auto" 或非负有限数`;
|
|
159
|
+
default:
|
|
160
|
+
return `${path} 必须是非空字符串`;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function applyField(target: Record<string, unknown>, field: FieldSpec, raw: unknown): boolean {
|
|
165
|
+
if (raw === undefined) return true;
|
|
166
|
+
const min = field.min ?? Number.NEGATIVE_INFINITY;
|
|
167
|
+
switch (field.kind) {
|
|
168
|
+
case 'bool':
|
|
169
|
+
if (typeof raw !== 'boolean') return false;
|
|
170
|
+
break;
|
|
171
|
+
case 'int':
|
|
172
|
+
if (typeof raw !== 'number' || !Number.isInteger(raw) || raw < min) return false;
|
|
173
|
+
break;
|
|
174
|
+
case 'number':
|
|
175
|
+
if (typeof raw !== 'number' || !Number.isFinite(raw) || raw < min) return false;
|
|
176
|
+
if (field.max !== undefined && raw > field.max) return false;
|
|
177
|
+
break;
|
|
178
|
+
case 'ratio':
|
|
179
|
+
if (raw !== 'auto' && (typeof raw !== 'number' || !Number.isFinite(raw) || raw < 0)) return false;
|
|
180
|
+
break;
|
|
181
|
+
case 'string':
|
|
182
|
+
if (typeof raw !== 'string' || !raw.trim()) return false;
|
|
183
|
+
target[field.key] = raw.trim();
|
|
184
|
+
return true;
|
|
185
|
+
case 'optionalString':
|
|
186
|
+
if (raw === '' || raw === null) {
|
|
187
|
+
target[field.key] = undefined;
|
|
188
|
+
return true;
|
|
189
|
+
}
|
|
190
|
+
if (typeof raw !== 'string' || !raw.trim()) return false;
|
|
191
|
+
target[field.key] = raw.trim();
|
|
192
|
+
return true;
|
|
193
|
+
}
|
|
194
|
+
target[field.key] = raw;
|
|
195
|
+
return true;
|
|
115
196
|
}
|
|
116
197
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
'enabled',
|
|
127
|
-
'logEnabled',
|
|
128
|
-
'cacheWriteReadRatio',
|
|
129
|
-
'keepRecentTokens',
|
|
130
|
-
'firstCompactionRequestScale',
|
|
131
|
-
'subsequentCompactionMargin',
|
|
132
|
-
]);
|
|
133
|
-
|
|
134
|
-
const OBS_KEYS = new Set([
|
|
135
|
-
'enabled',
|
|
136
|
-
'logEnabled',
|
|
137
|
-
'thresholdBytes',
|
|
138
|
-
'fullSends',
|
|
139
|
-
'recallChunkBytes',
|
|
140
|
-
'excerptBytes',
|
|
141
|
-
]);
|
|
142
|
-
|
|
143
|
-
const EPR_KEYS = new Set([
|
|
144
|
-
'enabled',
|
|
145
|
-
'logEnabled',
|
|
146
|
-
'model',
|
|
147
|
-
'minBytes',
|
|
148
|
-
'maxChars',
|
|
149
|
-
'maxOutputTokens',
|
|
150
|
-
'timeoutMs',
|
|
151
|
-
'localOnly',
|
|
152
|
-
]);
|
|
153
|
-
|
|
154
|
-
const JEV_KEYS: Record<string, true> = {
|
|
155
|
-
enabled: true,
|
|
156
|
-
logEnabled: true,
|
|
157
|
-
baseUrl: true,
|
|
158
|
-
model: true,
|
|
159
|
-
timeoutMs: true,
|
|
160
|
-
minConfidence: true,
|
|
161
|
-
apiKey: true,
|
|
162
|
-
};
|
|
198
|
+
function defaults(): EfficiencyConfig {
|
|
199
|
+
return {
|
|
200
|
+
version: 1,
|
|
201
|
+
onlineContextCompact: { ...DEFAULT_EFFICIENCY_CONFIG.onlineContextCompact },
|
|
202
|
+
observationPack: { ...DEFAULT_EFFICIENCY_CONFIG.observationPack },
|
|
203
|
+
evidencePreservingReducer: { ...DEFAULT_EFFICIENCY_CONFIG.evidencePreservingReducer },
|
|
204
|
+
jev: { ...DEFAULT_EFFICIENCY_CONFIG.jev },
|
|
205
|
+
};
|
|
206
|
+
}
|
|
163
207
|
|
|
164
208
|
export interface ValidateConfigResult {
|
|
165
209
|
ok: boolean;
|
|
@@ -168,301 +212,124 @@ export interface ValidateConfigResult {
|
|
|
168
212
|
}
|
|
169
213
|
|
|
170
214
|
export function validateEfficiencyConfig(raw: unknown): ValidateConfigResult {
|
|
215
|
+
const config = defaults();
|
|
171
216
|
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
|
-
};
|
|
217
|
+
for (const section of SECTIONS) config[section.name].enabled = false;
|
|
218
|
+
return { ok: false, config, issues: ['efficiency config 必须是 JSON 对象'] };
|
|
183
219
|
}
|
|
184
220
|
|
|
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
|
-
}
|
|
221
|
+
const issues: string[] = [];
|
|
222
|
+
for (const key of Object.keys(raw)) {
|
|
223
|
+
if (key !== 'version' && !SECTIONS.some((s) => s.name === key)) issues.push(`未知顶层配置项: "${key}"`);
|
|
195
224
|
}
|
|
196
|
-
|
|
197
225
|
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
|
-
}
|
|
226
|
+
issues.push(`version 必须是 1 (当前: ${JSON.stringify(raw.version)})`);
|
|
250
227
|
}
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
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
|
-
}
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
// Section 4: jev decision layer
|
|
364
|
-
let jev: JevConfig = { ...DEFAULT_EFFICIENCY_CONFIG.jev };
|
|
365
|
-
if (raw.jev !== undefined) {
|
|
366
|
-
if (!isPlainObject(raw.jev)) {
|
|
367
|
-
jevIssues.push('jev 必须是 JSON 对象');
|
|
368
|
-
} else {
|
|
369
|
-
const o = raw.jev;
|
|
370
|
-
for (const k of Object.keys(o)) {
|
|
371
|
-
if (JEV_KEYS[k] !== true) jevIssues.push(`jev 未知配置项: "${k}"`);
|
|
372
|
-
}
|
|
373
|
-
if (o.enabled !== undefined) {
|
|
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 的整数');
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
|
-
if (o.minConfidence !== undefined) {
|
|
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 之间的数字');
|
|
228
|
+
const topLevelInvalid = issues.length > 0;
|
|
229
|
+
|
|
230
|
+
for (const section of SECTIONS) {
|
|
231
|
+
const issueCountBefore = issues.length;
|
|
232
|
+
const target = config[section.name] as unknown as Record<string, unknown>;
|
|
233
|
+
const rawSection = raw[section.name];
|
|
234
|
+
if (rawSection !== undefined) {
|
|
235
|
+
if (!isPlainObject(rawSection)) {
|
|
236
|
+
issues.push(`${section.name} 必须是 JSON 对象`);
|
|
237
|
+
} else {
|
|
238
|
+
for (const key of Object.keys(rawSection)) {
|
|
239
|
+
if (!section.fields.some((f) => f.key === key)) issues.push(`${section.name} 未知配置项: "${key}"`);
|
|
409
240
|
}
|
|
410
|
-
|
|
411
|
-
|
|
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 必须是非空字符串');
|
|
241
|
+
for (const field of section.fields) {
|
|
242
|
+
if (!applyField(target, field, rawSection[field.key])) issues.push(issueFor(section.name, field));
|
|
418
243
|
}
|
|
419
244
|
}
|
|
420
245
|
}
|
|
246
|
+
// Fail-open: an invalid section (or an invalid top level) can never enable a mechanism.
|
|
247
|
+
if (topLevelInvalid || issues.length > issueCountBefore) config[section.name].enabled = false;
|
|
421
248
|
}
|
|
422
249
|
|
|
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
|
-
};
|
|
250
|
+
return { ok: issues.length === 0, config, issues };
|
|
456
251
|
}
|
|
457
252
|
|
|
458
|
-
function parseEnvBool(val: string
|
|
459
|
-
if (!val) return undefined;
|
|
253
|
+
function parseEnvBool(val: string): boolean | undefined {
|
|
460
254
|
const s = val.trim().toLowerCase();
|
|
461
255
|
if (s === '1' || s === 'true' || s === 'on') return true;
|
|
462
256
|
if (s === '0' || s === 'false' || s === 'off') return false;
|
|
463
257
|
return undefined;
|
|
464
258
|
}
|
|
465
259
|
|
|
260
|
+
interface EnvOverride {
|
|
261
|
+
readonly env: string;
|
|
262
|
+
readonly section: SectionSpec['name'];
|
|
263
|
+
readonly key: string;
|
|
264
|
+
readonly kind: 'bool' | 'string' | 'ratio' | 'int' | 'unit';
|
|
265
|
+
readonly min?: number;
|
|
266
|
+
/** Set only when the config left the key unset (foreign-convention fallback). */
|
|
267
|
+
readonly fallbackOnly?: boolean;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** Highest-precedence layer; an empty value counts as unset everywhere. */
|
|
271
|
+
const ENV_OVERRIDES: readonly EnvOverride[] = [
|
|
272
|
+
{ env: 'PI_HERDR_COMPACT_ENABLE', section: 'onlineContextCompact', key: 'enabled', kind: 'bool' },
|
|
273
|
+
{ env: 'PI_HERDR_COMPACT_LOG', section: 'onlineContextCompact', key: 'logEnabled', kind: 'bool' },
|
|
274
|
+
{ env: 'PI_HERDR_CACHE_RATIO', section: 'onlineContextCompact', key: 'cacheWriteReadRatio', kind: 'ratio' },
|
|
275
|
+
{ env: 'PI_HERDR_OBS_PACK_ENABLE', section: 'observationPack', key: 'enabled', kind: 'bool' },
|
|
276
|
+
{ env: 'PI_HERDR_OBS_PACK_LOG', section: 'observationPack', key: 'logEnabled', kind: 'bool' },
|
|
277
|
+
{ env: 'PI_HERDR_REDUCER_ENABLE', section: 'evidencePreservingReducer', key: 'enabled', kind: 'bool' },
|
|
278
|
+
{ env: 'PI_HERDR_REDUCER_LOG', section: 'evidencePreservingReducer', key: 'logEnabled', kind: 'bool' },
|
|
279
|
+
{ env: 'PI_HERDR_REDUCER_MODEL', section: 'evidencePreservingReducer', key: 'model', kind: 'string' },
|
|
280
|
+
// jev has no legacy spelling: PIER_JEV_* is canonical from the start.
|
|
281
|
+
{ env: 'PIER_JEV_ENABLE', section: 'jev', key: 'enabled', kind: 'bool' },
|
|
282
|
+
{ env: 'PIER_JEV_LOG', section: 'jev', key: 'logEnabled', kind: 'bool' },
|
|
283
|
+
{ env: 'PIER_JEV_MODEL', section: 'jev', key: 'model', kind: 'string' },
|
|
284
|
+
{ env: 'PIER_JEV_BASE_URL', section: 'jev', key: 'baseUrl', kind: 'string' },
|
|
285
|
+
{ env: 'PIER_JEV_TIMEOUT_MS', section: 'jev', key: 'timeoutMs', kind: 'int', min: 500 },
|
|
286
|
+
{ env: 'PIER_JEV_MIN_CONFIDENCE', section: 'jev', key: 'minConfidence', kind: 'unit' },
|
|
287
|
+
{ env: 'PIER_JEV_API_KEY', section: 'jev', key: 'apiKey', kind: 'string' },
|
|
288
|
+
// TYPESAFE_API_KEY is a foreign (SDK) convention, so it ranks below the config file too.
|
|
289
|
+
{ env: 'TYPESAFE_API_KEY', section: 'jev', key: 'apiKey', kind: 'string', fallbackOnly: true },
|
|
290
|
+
];
|
|
291
|
+
|
|
292
|
+
function applyEnvOverrides(
|
|
293
|
+
config: EfficiencyConfig,
|
|
294
|
+
env: Record<string, string | undefined>,
|
|
295
|
+
warn: (message: string) => void,
|
|
296
|
+
): void {
|
|
297
|
+
for (const override of ENV_OVERRIDES) {
|
|
298
|
+
const raw = env[override.env];
|
|
299
|
+
if (raw === undefined || raw === '') continue;
|
|
300
|
+
const target = config[override.section] as unknown as Record<string, unknown>;
|
|
301
|
+
if (override.fallbackOnly && target[override.key] !== undefined) continue;
|
|
302
|
+
|
|
303
|
+
if (override.kind === 'bool') {
|
|
304
|
+
const parsed = parseEnvBool(raw);
|
|
305
|
+
if (parsed !== undefined) target[override.key] = parsed;
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
if (override.kind === 'string') {
|
|
309
|
+
if (raw.trim()) target[override.key] = raw.trim();
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
if (override.kind === 'ratio' && raw.trim().toLowerCase() === 'auto') {
|
|
313
|
+
target[override.key] = 'auto';
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
const num = Number(raw);
|
|
317
|
+
const valid =
|
|
318
|
+
override.kind === 'ratio' ? Number.isFinite(num) && num >= 0
|
|
319
|
+
: override.kind === 'unit' ? Number.isFinite(num) && num >= 0 && num <= 1
|
|
320
|
+
: Number.isInteger(num) && num >= (override.min ?? 0);
|
|
321
|
+
if (valid) {
|
|
322
|
+
target[override.key] = num;
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
const requirement =
|
|
326
|
+
override.kind === 'unit' ? '(需 0 到 1)'
|
|
327
|
+
: override.kind === 'int' ? `(需 >= ${override.min} 的整数)`
|
|
328
|
+
: '';
|
|
329
|
+
warn(`无效环境变量 ${override.env}="${raw}"${requirement},忽略`);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
466
333
|
export interface ResolveConfigOptions {
|
|
467
334
|
workspaceConfig?: unknown;
|
|
468
335
|
userConfig?: unknown;
|
|
@@ -473,128 +340,28 @@ export interface ResolveConfigOptions {
|
|
|
473
340
|
|
|
474
341
|
export function resolveEfficiencyConfig(opts: ResolveConfigOptions = {}): EfficiencyConfig {
|
|
475
342
|
const env = opts.env ?? process.env;
|
|
476
|
-
const warn = opts.onWarning ?? ((msg) => console.warn(`[pi-herdr] efficiency config warning: ${msg}`));
|
|
343
|
+
const warn = opts.onWarning ?? ((msg: string) => console.warn(`[pi-herdr] efficiency config warning: ${msg}`));
|
|
477
344
|
const isTrusted = opts.isProjectTrusted ?? false;
|
|
478
345
|
|
|
479
|
-
|
|
480
|
-
|
|
346
|
+
// An untrusted project's file is ignored wholesale (never merged with the user layer).
|
|
347
|
+
let baseRaw: unknown = opts.userConfig;
|
|
481
348
|
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;
|
|
349
|
+
if (isTrusted) baseRaw = opts.workspaceConfig;
|
|
350
|
+
else warn('工作区能效配置未被信任(isProjectTrusted=false),已安全忽略工作区配置');
|
|
490
351
|
}
|
|
491
352
|
|
|
492
|
-
let resolved
|
|
353
|
+
let resolved = defaults();
|
|
493
354
|
if (baseRaw !== undefined) {
|
|
494
355
|
const validated = validateEfficiencyConfig(baseRaw);
|
|
495
|
-
if (!validated.ok) {
|
|
496
|
-
warn(`配置存在校验问题,已安全回退默认值: ${validated.issues.join('; ')}`);
|
|
497
|
-
}
|
|
356
|
+
if (!validated.ok) warn(`配置存在校验问题,已安全回退默认值: ${validated.issues.join('; ')}`);
|
|
498
357
|
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
358
|
}
|
|
593
359
|
|
|
360
|
+
applyEnvOverrides(resolved, env, warn);
|
|
594
361
|
return resolved;
|
|
595
362
|
}
|
|
596
363
|
|
|
597
|
-
|
|
364
|
+
function defaultUserEfficiencyConfigDir(): string {
|
|
598
365
|
return join(homedir(), '.pi', 'agent', 'herdr-pi');
|
|
599
366
|
}
|
|
600
367
|
|
|
@@ -606,6 +373,44 @@ export function defaultUserEfficiencyConfigFile(): string {
|
|
|
606
373
|
return join(defaultUserEfficiencyConfigDir(), 'config.json');
|
|
607
374
|
}
|
|
608
375
|
|
|
376
|
+
interface PiCompactionSettings {
|
|
377
|
+
enabled?: boolean;
|
|
378
|
+
keepRecentTokens?: number;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function readCompactionSettings(filePath: string): PiCompactionSettings {
|
|
382
|
+
const parsed = readJsonConfig(filePath);
|
|
383
|
+
if (!isPlainObject(parsed) || !isPlainObject(parsed.compaction)) return {};
|
|
384
|
+
const { compaction } = parsed;
|
|
385
|
+
const result: PiCompactionSettings = {};
|
|
386
|
+
if (typeof compaction.enabled === 'boolean') result.enabled = compaction.enabled;
|
|
387
|
+
if (typeof compaction.keepRecentTokens === 'number') result.keepRecentTokens = compaction.keepRecentTokens;
|
|
388
|
+
return result;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function readJsonConfig(filePath: string, onError?: (error: unknown) => void): unknown {
|
|
392
|
+
if (!existsSync(filePath)) return undefined;
|
|
393
|
+
try {
|
|
394
|
+
return JSON.parse(readFileSync(filePath, 'utf8')) as unknown;
|
|
395
|
+
} catch (error) {
|
|
396
|
+
onError?.(error);
|
|
397
|
+
return undefined;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/** Pi's own compaction settings: global, plus the project file when the project is trusted. */
|
|
402
|
+
export function loadPiNativeCompactionSettings(opts: {
|
|
403
|
+
cwd?: string;
|
|
404
|
+
isProjectTrusted?: boolean;
|
|
405
|
+
agentDir?: string;
|
|
406
|
+
} = {}): PiCompactionSettings {
|
|
407
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
408
|
+
const agentDir = opts.agentDir ?? process.env.PI_CODING_AGENT_DIR ?? join(homedir(), '.pi', 'agent');
|
|
409
|
+
const globalSettings = readCompactionSettings(join(agentDir, 'settings.json'));
|
|
410
|
+
if (!opts.isProjectTrusted) return globalSettings;
|
|
411
|
+
return { ...globalSettings, ...readCompactionSettings(join(cwd, '.pi', 'settings.json')) };
|
|
412
|
+
}
|
|
413
|
+
|
|
609
414
|
export function loadEfficiencyConfigFromDisk(opts: {
|
|
610
415
|
cwd?: string;
|
|
611
416
|
isProjectTrusted?: boolean;
|
|
@@ -613,13 +418,11 @@ export function loadEfficiencyConfigFromDisk(opts: {
|
|
|
613
418
|
onWarning?: (msg: string) => void;
|
|
614
419
|
/** Override the Pi agent dir used to read native `settings.json` (tests/isolation). */
|
|
615
420
|
agentDir?: string;
|
|
616
|
-
/** Override the user-level efficiency config file path (tests/isolation). */
|
|
617
421
|
userConfigPath?: string;
|
|
618
422
|
} = {}): EfficiencyConfig {
|
|
619
423
|
const cwd = opts.cwd ?? process.cwd();
|
|
620
424
|
const wsPath = defaultWorkspaceEfficiencyConfigFile(cwd);
|
|
621
425
|
const userPath = opts.userConfigPath ?? defaultUserEfficiencyConfigFile();
|
|
622
|
-
|
|
623
426
|
const warnFile = (label: string, filePath: string, error: unknown): void => {
|
|
624
427
|
opts.onWarning?.(`${label} ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
625
428
|
};
|
|
@@ -639,23 +442,20 @@ export function loadEfficiencyConfigFromDisk(opts: {
|
|
|
639
442
|
agentDir: opts.agentDir,
|
|
640
443
|
});
|
|
641
444
|
|
|
642
|
-
//
|
|
445
|
+
// pi's global compaction switch wins over an explicitly enabled OCC — only an env force overrides it.
|
|
643
446
|
const env = opts.env ?? process.env;
|
|
644
447
|
if (piSettings.enabled === false && !env.PI_HERDR_COMPACT_ENABLE) {
|
|
645
448
|
resolved.onlineContextCompact.enabled = false;
|
|
646
449
|
}
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
450
|
+
const hasExplicitKeepRecent = [opts.isProjectTrusted ? workspaceConfig : undefined, userConfig].some(
|
|
451
|
+
(config) =>
|
|
452
|
+
isPlainObject(config)
|
|
453
|
+
&& isPlainObject(config.onlineContextCompact)
|
|
454
|
+
&& config.onlineContextCompact.keepRecentTokens !== undefined,
|
|
455
|
+
);
|
|
456
|
+
// pi's keepRecentTokens is inherited only while the efficiency config does not set its own.
|
|
655
457
|
if (!hasExplicitKeepRecent && typeof piSettings.keepRecentTokens === 'number') {
|
|
656
458
|
resolved.onlineContextCompact.keepRecentTokens = piSettings.keepRecentTokens;
|
|
657
459
|
}
|
|
658
|
-
|
|
659
460
|
return resolved;
|
|
660
461
|
}
|
|
661
|
-
|