pi-pier 0.2.6 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/README.md +20 -7
  2. package/package.json +7 -2
  3. package/schemas/efficiency-config.schema.json +77 -0
  4. package/skills/pi-herdr-workflow/SKILL.md +13 -1
  5. package/src/ansi-text.ts +83 -0
  6. package/src/ask-multi.ts +260 -0
  7. package/src/ask-user.ts +458 -0
  8. package/src/bootstrap.ts +5 -2
  9. package/src/compact-coordinator.ts +454 -0
  10. package/src/compact-economics-core.ts +396 -0
  11. package/src/config-catalog-core.ts +452 -0
  12. package/src/config-command.ts +181 -0
  13. package/src/config-guide.ts +361 -0
  14. package/src/core/grid-shape.ts +4 -2
  15. package/src/core/heat-plan.ts +160 -0
  16. package/src/core/observation.ts +555 -0
  17. package/src/core/subagent.ts +187 -393
  18. package/src/core/terminal.ts +270 -47
  19. package/src/core/todo.ts +49 -14
  20. package/src/dashboard-command.ts +89 -0
  21. package/src/efficiency-config-core.ts +661 -0
  22. package/src/efficiency-config-io.ts +53 -0
  23. package/src/efficiency-store.ts +312 -0
  24. package/src/focus-poller.ts +244 -0
  25. package/src/gc-core.ts +85 -0
  26. package/src/git-adapter.ts +12 -3
  27. package/src/herdr-client.ts +292 -36
  28. package/src/index-locks.ts +27 -9
  29. package/src/index-master.ts +5 -0
  30. package/src/index-notices.ts +13 -2
  31. package/src/index-pipe.ts +5 -0
  32. package/src/index-worker.ts +5 -0
  33. package/src/index.ts +360 -60
  34. package/src/jev-client.ts +146 -0
  35. package/src/jev-core.ts +381 -0
  36. package/src/lock-core.ts +105 -16
  37. package/src/observation-core.ts +248 -0
  38. package/src/pier-options.ts +96 -0
  39. package/src/pipe-channel.ts +52 -7
  40. package/src/reducer-core.ts +358 -0
  41. package/src/reducer-invoker.ts +392 -0
  42. package/src/renderers.ts +198 -0
  43. package/src/role-loader.ts +2 -1
  44. package/src/role-manifest.ts +0 -4
  45. package/src/runtime-policy.ts +5 -8
  46. package/src/session-tail.ts +60 -5
  47. package/src/settle-wake-core.ts +3 -1
  48. package/src/slim-frame.ts +60 -3
  49. package/src/storage-layout.ts +23 -0
  50. package/src/subagent-core.ts +3 -1
  51. package/src/subagent-execute.ts +328 -0
  52. package/src/subagent-gc.ts +33 -13
  53. package/src/subagent-launch.ts +4 -0
  54. package/src/subagent-list-action.ts +69 -0
  55. package/src/subagent-output-action.ts +104 -0
  56. package/src/subagent-output-adapter.ts +27 -0
  57. package/src/subagent-output-core.ts +263 -0
  58. package/src/subagent-poll-loop.ts +59 -39
  59. package/src/subagent-poller.ts +46 -0
  60. package/src/subagent-ready.ts +89 -0
  61. package/src/subagent-registry.ts +70 -0
  62. package/src/subagent-resolution.ts +41 -0
  63. package/src/subagent-scope.ts +3 -1
  64. package/src/subagent-session-io.ts +115 -32
  65. package/src/subagent-spawn.ts +153 -8
  66. package/src/swallow.ts +75 -0
  67. package/src/terminal-core.ts +161 -10
  68. package/src/todo-core.ts +5 -4
  69. package/src/todo-reminder-core.ts +8 -1
  70. package/src/todos-service.ts +18 -6
  71. package/src/tool-error.ts +26 -0
  72. package/src/vocab.ts +7 -4
@@ -0,0 +1,661 @@
1
+ /**
2
+ * D100-D103 Efficiency Configuration Core.
3
+ *
4
+ * Provides zero-dependency parsing, validation, and multi-tier resolution
5
+ * for pier efficiency mechanisms (OCC, ObservationPack, EPR).
6
+ *
7
+ * Contract:
8
+ * - Unknown keys rejected to catch typos; all issues collected in one pass.
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.
14
+ */
15
+
16
+ import { homedir } from 'node:os';
17
+ import { join } from 'node:path';
18
+ import { loadPiNativeCompactionSettings, readJsonConfig } from './efficiency-config-io.ts';
19
+
20
+ export interface OnlineContextCompactConfig {
21
+ enabled: boolean;
22
+ logEnabled: boolean;
23
+ cacheWriteReadRatio: number | 'auto';
24
+ keepRecentTokens?: number;
25
+ firstCompactionRequestScale: number;
26
+ subsequentCompactionMargin: number;
27
+ }
28
+
29
+ export interface ObservationPackConfig {
30
+ enabled: boolean;
31
+ logEnabled: boolean;
32
+ thresholdBytes: number;
33
+ fullSends: number;
34
+ recallChunkBytes: number;
35
+ excerptBytes: number;
36
+ }
37
+
38
+ export interface EvidencePreservingReducerConfig {
39
+ enabled: boolean;
40
+ logEnabled: boolean;
41
+ model?: string;
42
+ minBytes: number;
43
+ maxChars: number;
44
+ maxOutputTokens: number;
45
+ timeoutMs: number;
46
+ localOnly: boolean;
47
+ }
48
+
49
+ /**
50
+ * Jev decision layer (RFC docs/rfc-jev-integration.md). Optional System One
51
+ * classification calls that upgrade hand-written heuristics; every call site
52
+ * fails open to the existing heuristic on any error, timeout, or low confidence.
53
+ */
54
+ export interface JevConfig {
55
+ enabled: boolean;
56
+ logEnabled: boolean;
57
+ /** API root override (relay/gateway); default https://api.typesafe.ai. */
58
+ baseUrl?: string;
59
+ /** Pinned versioned model id — aliases (jev-latest) move silently and would skew tuned thresholds. */
60
+ model: string;
61
+ /** Total per-call budget in ms (AbortController hard kill; the SDK has no total-budget mode). */
62
+ timeoutMs: number;
63
+ /** Minimum Choice/Score confidence to accept an answer; below = treat the question as unanswered. */
64
+ minConfidence: number;
65
+ /** Resolution: PIER_JEV_API_KEY env > this value > TYPESAFE_API_KEY env (SDK convention). */
66
+ apiKey?: string;
67
+ }
68
+
69
+ export interface EfficiencyConfig {
70
+ version: 1;
71
+ onlineContextCompact: OnlineContextCompactConfig;
72
+ observationPack: ObservationPackConfig;
73
+ evidencePreservingReducer: EvidencePreservingReducerConfig;
74
+ jev: JevConfig;
75
+ }
76
+
77
+ export const DEFAULT_EFFICIENCY_CONFIG: EfficiencyConfig = Object.freeze({
78
+ version: 1,
79
+ onlineContextCompact: Object.freeze({
80
+ enabled: false,
81
+ logEnabled: false,
82
+ cacheWriteReadRatio: 'auto',
83
+ keepRecentTokens: 20000,
84
+ firstCompactionRequestScale: 2.0,
85
+ subsequentCompactionMargin: 1.5,
86
+ }),
87
+ observationPack: Object.freeze({
88
+ enabled: false,
89
+ logEnabled: false,
90
+ thresholdBytes: 10240,
91
+ fullSends: 2,
92
+ recallChunkBytes: 16384,
93
+ excerptBytes: 1024,
94
+ }),
95
+ evidencePreservingReducer: Object.freeze({
96
+ enabled: false,
97
+ logEnabled: false,
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,
110
+ }),
111
+ });
112
+
113
+ function isPlainObject(v: unknown): v is Record<string, unknown> {
114
+ return typeof v === 'object' && v !== null && !Array.isArray(v);
115
+ }
116
+
117
+ const TOP_LEVEL_KEYS = new Set([
118
+ 'version',
119
+ 'onlineContextCompact',
120
+ 'observationPack',
121
+ 'evidencePreservingReducer',
122
+ 'jev',
123
+ ]);
124
+
125
+ const OCC_KEYS = new Set([
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
+ };
163
+
164
+ export interface ValidateConfigResult {
165
+ ok: boolean;
166
+ config: EfficiencyConfig;
167
+ issues: string[];
168
+ }
169
+
170
+ export function validateEfficiencyConfig(raw: unknown): ValidateConfigResult {
171
+ if (!isPlainObject(raw)) {
172
+ return {
173
+ ok: false,
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
+ };
183
+ }
184
+
185
+ const topIssues: string[] = [];
186
+ const occIssues: string[] = [];
187
+ const obsIssues: string[] = [];
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
+ }
195
+ }
196
+
197
+ if (raw.version !== undefined && raw.version !== 1) {
198
+ topIssues.push(`version 必须是 1 (当前: ${JSON.stringify(raw.version)})`);
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
+ }
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 之间的数字');
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 必须是非空字符串');
418
+ }
419
+ }
420
+ }
421
+ }
422
+
423
+ // Fail-open: any section with validation issues has its enabled flag forced to false
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
+ };
456
+ }
457
+
458
+ function parseEnvBool(val: string | undefined): boolean | undefined {
459
+ if (!val) return undefined;
460
+ const s = val.trim().toLowerCase();
461
+ if (s === '1' || s === 'true' || s === 'on') return true;
462
+ if (s === '0' || s === 'false' || s === 'off') return false;
463
+ return undefined;
464
+ }
465
+
466
+ export interface ResolveConfigOptions {
467
+ workspaceConfig?: unknown;
468
+ userConfig?: unknown;
469
+ env?: Record<string, string | undefined>;
470
+ isProjectTrusted?: boolean;
471
+ onWarning?: (message: string) => void;
472
+ }
473
+
474
+ export function resolveEfficiencyConfig(opts: ResolveConfigOptions = {}): EfficiencyConfig {
475
+ const env = opts.env ?? process.env;
476
+ const warn = opts.onWarning ?? ((msg) => console.warn(`[pi-herdr] efficiency config warning: ${msg}`));
477
+ const isTrusted = opts.isProjectTrusted ?? false;
478
+
479
+ let baseRaw: unknown = undefined;
480
+
481
+ if (opts.workspaceConfig !== undefined) {
482
+ if (isTrusted) {
483
+ baseRaw = opts.workspaceConfig;
484
+ } else {
485
+ warn('工作区能效配置未被信任(isProjectTrusted=false),已安全忽略工作区配置');
486
+ baseRaw = opts.userConfig;
487
+ }
488
+ } else {
489
+ baseRaw = opts.userConfig;
490
+ }
491
+
492
+ let resolved: EfficiencyConfig;
493
+ if (baseRaw !== undefined) {
494
+ const validated = validateEfficiencyConfig(baseRaw);
495
+ if (!validated.ok) {
496
+ warn(`配置存在校验问题,已安全回退默认值: ${validated.issues.join('; ')}`);
497
+ }
498
+ 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
+ }
593
+
594
+ return resolved;
595
+ }
596
+
597
+ export function defaultUserEfficiencyConfigDir(): string {
598
+ return join(homedir(), '.pi', 'agent', 'herdr-pi');
599
+ }
600
+
601
+ export function defaultWorkspaceEfficiencyConfigFile(cwd: string): string {
602
+ return join(cwd, '.pi-herdr', 'config.json');
603
+ }
604
+
605
+ export function defaultUserEfficiencyConfigFile(): string {
606
+ return join(defaultUserEfficiencyConfigDir(), 'config.json');
607
+ }
608
+
609
+ export function loadEfficiencyConfigFromDisk(opts: {
610
+ cwd?: string;
611
+ isProjectTrusted?: boolean;
612
+ env?: Record<string, string | undefined>;
613
+ onWarning?: (msg: string) => void;
614
+ /** Override the Pi agent dir used to read native `settings.json` (tests/isolation). */
615
+ agentDir?: string;
616
+ /** Override the user-level efficiency config file path (tests/isolation). */
617
+ userConfigPath?: string;
618
+ } = {}): EfficiencyConfig {
619
+ const cwd = opts.cwd ?? process.cwd();
620
+ const wsPath = defaultWorkspaceEfficiencyConfigFile(cwd);
621
+ const userPath = opts.userConfigPath ?? defaultUserEfficiencyConfigFile();
622
+
623
+ const warnFile = (label: string, filePath: string, error: unknown): void => {
624
+ opts.onWarning?.(`${label} ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
625
+ };
626
+ const workspaceConfig = readJsonConfig(wsPath, (error) => warnFile('无法解析工作区能效配置文件', wsPath, error));
627
+ const userConfig = readJsonConfig(userPath, (error) => warnFile('无法解析用户能效配置文件', userPath, error));
628
+ const resolved = resolveEfficiencyConfig({
629
+ workspaceConfig,
630
+ userConfig,
631
+ isProjectTrusted: opts.isProjectTrusted,
632
+ env: opts.env,
633
+ onWarning: opts.onWarning,
634
+ });
635
+
636
+ const piSettings = loadPiNativeCompactionSettings({
637
+ cwd,
638
+ isProjectTrusted: opts.isProjectTrusted,
639
+ agentDir: opts.agentDir,
640
+ });
641
+
642
+ // If user disabled compaction globally in pi settings, and env didn't explicitly force OCC enable, respect it!
643
+ const env = opts.env ?? process.env;
644
+ if (piSettings.enabled === false && !env.PI_HERDR_COMPACT_ENABLE) {
645
+ resolved.onlineContextCompact.enabled = false;
646
+ }
647
+
648
+ const hasExplicitKeepRecent = [
649
+ opts.isProjectTrusted ? workspaceConfig : undefined,
650
+ userConfig,
651
+ ].some((config) => isPlainObject(config)
652
+ && isPlainObject(config.onlineContextCompact)
653
+ && config.onlineContextCompact.keepRecentTokens !== undefined);
654
+
655
+ if (!hasExplicitKeepRecent && typeof piSettings.keepRecentTokens === 'number') {
656
+ resolved.onlineContextCompact.keepRecentTokens = piSettings.keepRecentTokens;
657
+ }
658
+
659
+ return resolved;
660
+ }
661
+