pi-warden 0.60.0 → 0.71.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.
- package/CHANGELOG.md +140 -0
- package/README.md +49 -93
- package/dist/adaptive.d.ts +132 -0
- package/dist/adaptive.js +274 -0
- package/dist/atomic.d.ts +10 -0
- package/dist/atomic.js +51 -0
- package/dist/compact.d.ts +9 -0
- package/dist/compact.js +34 -10
- package/dist/config.d.ts +47 -2
- package/dist/config.js +43 -11
- package/dist/dedupe.d.ts +41 -0
- package/dist/dedupe.js +162 -0
- package/dist/evidence.d.ts +93 -0
- package/dist/evidence.js +409 -0
- package/dist/extension.d.ts +1 -1
- package/dist/extension.js +460 -62
- package/dist/extension.js.bak +668 -102
- package/dist/guard.d.ts +4 -0
- package/dist/guard.js +132 -9
- package/dist/index.d.ts +10 -2
- package/dist/index.js +5 -1
- package/dist/loops.d.ts +64 -0
- package/dist/loops.js +147 -0
- package/dist/prefs.d.ts +161 -10
- package/dist/prefs.js +371 -48
- package/dist/rules-lint.d.ts +84 -0
- package/dist/rules-lint.js +191 -0
- package/dist/rules-log.d.ts +98 -0
- package/dist/rules-log.js +190 -0
- package/dist/rules-report.d.ts +52 -0
- package/dist/rules-report.js +83 -0
- package/dist/rules.d.ts +24 -2
- package/dist/rules.js +154 -33
- package/dist/saver.d.ts +9 -1
- package/dist/saver.js +14 -4
- package/dist/shape.d.ts +1 -1
- package/dist/shape.js +23 -5
- package/dist/stuck.d.ts +34 -11
- package/dist/stuck.js +20 -8
- package/dist/trace.d.ts +1 -1
- package/dist/waste.d.ts +98 -0
- package/dist/waste.js +582 -0
- package/package.json +4 -2
package/dist/config.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
|
+
import { writeFileAtomicSync } from "./atomic.js";
|
|
4
5
|
import { resolveBackend } from "./backend.js";
|
|
5
6
|
import { COMMAND_TOOLS } from "./tools.js";
|
|
6
7
|
import { defaultWidgetConfig } from "./widget.js";
|
|
@@ -23,7 +24,7 @@ export function isRecallTool(value) {
|
|
|
23
24
|
}
|
|
24
25
|
export const PACKAGE_NAME = "pi-warden";
|
|
25
26
|
/** Bumped when WardenConfig gains a section; extension.ts checks it so a half-updated module graph is reported, not crashed on. */
|
|
26
|
-
export const CONFIG_SCHEMA =
|
|
27
|
+
export const CONFIG_SCHEMA = 10;
|
|
27
28
|
export const PROJECT_CONFIG_FILE = `${PACKAGE_NAME}.json`;
|
|
28
29
|
export function defaultConfig() {
|
|
29
30
|
return {
|
|
@@ -42,6 +43,7 @@ export function defaultConfig() {
|
|
|
42
43
|
offTask: { warn: 0.6, steer: 0.85 },
|
|
43
44
|
intentMismatch: 0.9,
|
|
44
45
|
visibleMismatch: 0.8,
|
|
46
|
+
intentTraceOnly: "invisible",
|
|
45
47
|
shouldProceed: { hold: 0.6, steer: false },
|
|
46
48
|
feedbackLog: true,
|
|
47
49
|
commandRules: [],
|
|
@@ -52,12 +54,12 @@ export function defaultConfig() {
|
|
|
52
54
|
escalationThreshold: 0.85,
|
|
53
55
|
floor: "evidence",
|
|
54
56
|
},
|
|
55
|
-
stuck: { enabled: true, window: 12, minFailures: 3, cooldown: 3, sameStrategy: 0.7, churnThreshold: 5, nudge: true, repeatSteer: true, diffLimit: 3000, tailLimit: 1000 },
|
|
57
|
+
stuck: { enabled: true, window: 12, minFailures: 3, cooldown: 3, sameStrategy: 0.7, churnThreshold: 5, nudge: true, repeatSteer: true, evidence: true, diffLimit: 3000, tailLimit: 1000 },
|
|
56
58
|
done: { enabled: true, claimsDone: 0.7, nudge: true, uiProof: true, uiFiles: [...DEFAULT_UI_FILES], visualTools: defaultVisualTools() },
|
|
57
59
|
slop: { enabled: true, threshold: 0.7, prose: { enabled: true, audience: "technical", threshold: 0.7, trend: 2, minChars: 200 } },
|
|
58
60
|
security: { enabled: true, threshold: 0.7, maskOutput: true },
|
|
59
|
-
rules: { enabled: true, threshold: 0.7, files: [], fallback: true, maxChars: 8000, exclude: [], skip: [], sensitivePaths: {} },
|
|
60
|
-
context: { enabled: true, tailMinChars: 12000, confidence: 0.8, duplicateMinChars: 2000, recallTool: "auto", formatConfidence: 0.7, compactAppendix: true, largeOutput: { enabled: true, threshold: 0.85 } },
|
|
61
|
+
rules: { enabled: true, threshold: 0.7, softThreshold: 0, files: [], fallback: true, maxChars: 8000, exclude: [], skip: [], sensitivePaths: {} },
|
|
62
|
+
context: { enabled: true, tailMinChars: 12000, confidence: 0.8, duplicateMinChars: 2000, recallTool: "auto", formatConfidence: 0.7, compactAppendix: true, dedupeRuns: true, dedupeMessages: false, largeOutput: { enabled: true, threshold: 0.85 } },
|
|
61
63
|
runaway: { enabled: true, repeats: 4, thinkingRepeats: 10, minChars: 400, recover: true },
|
|
62
64
|
notify: { enabled: false, cooldownMs: 10000, command: [] },
|
|
63
65
|
judge: { cooldownMs: 60000, failuresBeforeCooldown: 3 },
|
|
@@ -66,6 +68,7 @@ export function defaultConfig() {
|
|
|
66
68
|
steerVisible: false,
|
|
67
69
|
notices: false,
|
|
68
70
|
steerBudget: 3,
|
|
71
|
+
steers: { adaptive: true, minSteers: 30, minFollowed: 0.2, maxDisputed: 0.4, recheckEvery: 30, probeEvery: 5 },
|
|
69
72
|
learning: { adaptiveThresholds: true, patternAnalysis: true, minHoldsForAdaptive: 20, adaptationRate: 0.1, retentionDays: 365 },
|
|
70
73
|
conscience: {
|
|
71
74
|
enabled: false,
|
|
@@ -82,7 +85,8 @@ export function defaultConfig() {
|
|
|
82
85
|
advanceThreshold: 0.70,
|
|
83
86
|
loadThreshold: 1.0,
|
|
84
87
|
},
|
|
85
|
-
prefs: { enabled: true, inject:
|
|
88
|
+
prefs: { enabled: true, inject: true },
|
|
89
|
+
waste: { enabled: true, tip: false, every: 20, sleep: true, paging: true, search: true, recheck: true },
|
|
86
90
|
};
|
|
87
91
|
}
|
|
88
92
|
/** Mirrors Pi's agent directory rule so the file sits next to pi-typesafe's auth.json. */
|
|
@@ -284,6 +288,7 @@ function applyAction(base, raw, timeoutMs, source) {
|
|
|
284
288
|
offTask: offTaskThreshold(raw.offTask, base.offTask),
|
|
285
289
|
intentMismatch: probability(raw.intentMismatch, base.intentMismatch),
|
|
286
290
|
visibleMismatch: Math.min(probability(raw.visibleMismatch, base.visibleMismatch), probability(raw.intentMismatch, base.intentMismatch)),
|
|
291
|
+
intentTraceOnly: raw.intentTraceOnly === "invisible" || raw.intentTraceOnly === "all" || raw.intentTraceOnly === "none" ? raw.intentTraceOnly : base.intentTraceOnly,
|
|
287
292
|
shouldProceed: {
|
|
288
293
|
hold: probability(raw.shouldProceed?.hold, base.shouldProceed.hold),
|
|
289
294
|
steer: boolean(raw.shouldProceed?.steer, base.shouldProceed.steer),
|
|
@@ -315,6 +320,7 @@ function applyStuck(base, raw) {
|
|
|
315
320
|
churnThreshold: Math.min(window, positiveInteger(raw.churnThreshold, base.churnThreshold)),
|
|
316
321
|
nudge: boolean(raw.nudge, base.nudge),
|
|
317
322
|
repeatSteer: boolean(raw.repeatSteer, base.repeatSteer),
|
|
323
|
+
evidence: boolean(raw.evidence, base.evidence),
|
|
318
324
|
diffLimit: positiveInteger(raw.diffLimit, base.diffLimit),
|
|
319
325
|
tailLimit: positiveInteger(raw.tailLimit, base.tailLimit),
|
|
320
326
|
};
|
|
@@ -400,6 +406,7 @@ function applyRules(base, raw) {
|
|
|
400
406
|
return {
|
|
401
407
|
enabled: boolean(raw.enabled, base.enabled),
|
|
402
408
|
threshold: probability(raw.threshold, base.threshold),
|
|
409
|
+
softThreshold: probability(raw.softThreshold, base.softThreshold),
|
|
403
410
|
files: globList(raw.files, base.files),
|
|
404
411
|
fallback: boolean(raw.fallback, base.fallback),
|
|
405
412
|
maxChars: Math.max(500, positiveInteger(raw.maxChars, base.maxChars)),
|
|
@@ -440,6 +447,7 @@ function applyShared(base, raw) {
|
|
|
440
447
|
}
|
|
441
448
|
function applyGuards(base, raw, timeoutMs, source) {
|
|
442
449
|
return {
|
|
450
|
+
waste: applyWaste(base.waste, raw.waste),
|
|
443
451
|
rules: applyRules(base.rules, raw.rules),
|
|
444
452
|
runaway: applyRunaway(base.runaway, raw.runaway),
|
|
445
453
|
subagent: applySubagent(base.subagent, raw.subagent),
|
|
@@ -464,6 +472,8 @@ function applyGuards(base, raw, timeoutMs, source) {
|
|
|
464
472
|
recallTool: isRecallTool(raw.context.recallTool) ? raw.context.recallTool : base.context.recallTool,
|
|
465
473
|
formatConfidence: probability(raw.context.formatConfidence, base.context.formatConfidence),
|
|
466
474
|
compactAppendix: boolean(raw.context.compactAppendix, base.context.compactAppendix),
|
|
475
|
+
dedupeRuns: boolean(raw.context.dedupeRuns, base.context.dedupeRuns),
|
|
476
|
+
dedupeMessages: boolean(raw.context.dedupeMessages, base.context.dedupeMessages),
|
|
467
477
|
largeOutput: isObject(raw.context.largeOutput) ? {
|
|
468
478
|
enabled: boolean(raw.context.largeOutput.enabled, base.context.largeOutput.enabled),
|
|
469
479
|
threshold: probability(raw.context.largeOutput.threshold, base.context.largeOutput.threshold),
|
|
@@ -487,16 +497,42 @@ export function applyUserOverrides(base, raw) {
|
|
|
487
497
|
steerVisible: boolean(raw.steerVisible, base.steerVisible),
|
|
488
498
|
notices: boolean(raw.notices, base.notices),
|
|
489
499
|
steerBudget: typeof raw.steerBudget === "number" && Number.isInteger(raw.steerBudget) && raw.steerBudget >= 0 ? raw.steerBudget : base.steerBudget,
|
|
500
|
+
steers: applySteers(base.steers, raw.steers),
|
|
490
501
|
learning: applyLearning(base.learning, raw.learning),
|
|
491
502
|
conscience: applyConscience(base.conscience, raw.conscience),
|
|
492
503
|
prefs: applyPrefs(base.prefs, raw.prefs),
|
|
493
504
|
};
|
|
494
505
|
}
|
|
506
|
+
function applyWaste(base, raw) {
|
|
507
|
+
if (!isObject(raw))
|
|
508
|
+
return base;
|
|
509
|
+
return {
|
|
510
|
+
enabled: boolean(raw.enabled, base.enabled),
|
|
511
|
+
tip: boolean(raw.tip, base.tip),
|
|
512
|
+
every: Math.max(2, positiveInteger(raw.every, base.every)),
|
|
513
|
+
sleep: boolean(raw.sleep, base.sleep),
|
|
514
|
+
paging: boolean(raw.paging, base.paging),
|
|
515
|
+
search: boolean(raw.search, base.search),
|
|
516
|
+
recheck: boolean(raw.recheck, base.recheck),
|
|
517
|
+
};
|
|
518
|
+
}
|
|
495
519
|
function applyPrefs(base, raw) {
|
|
496
520
|
if (!isObject(raw))
|
|
497
521
|
return base;
|
|
498
522
|
return { enabled: boolean(raw.enabled, base.enabled), inject: boolean(raw.inject, base.inject) };
|
|
499
523
|
}
|
|
524
|
+
function applySteers(base, raw) {
|
|
525
|
+
if (!isObject(raw))
|
|
526
|
+
return base;
|
|
527
|
+
return {
|
|
528
|
+
adaptive: boolean(raw.adaptive, base.adaptive),
|
|
529
|
+
minSteers: positiveInteger(raw.minSteers, base.minSteers),
|
|
530
|
+
minFollowed: probability(raw.minFollowed, base.minFollowed),
|
|
531
|
+
maxDisputed: probability(raw.maxDisputed, base.maxDisputed),
|
|
532
|
+
recheckEvery: positiveInteger(raw.recheckEvery, base.recheckEvery),
|
|
533
|
+
probeEvery: positiveInteger(raw.probeEvery, base.probeEvery),
|
|
534
|
+
};
|
|
535
|
+
}
|
|
500
536
|
function applyLearning(base, raw) {
|
|
501
537
|
if (!isObject(raw))
|
|
502
538
|
return base;
|
|
@@ -553,11 +589,7 @@ export function readUserConfig() {
|
|
|
553
589
|
}
|
|
554
590
|
export function writeUserConfig(raw) {
|
|
555
591
|
const path = userConfigPath();
|
|
556
|
-
|
|
557
|
-
const temporary = `${path}.${process.pid}.tmp`;
|
|
558
|
-
writeFileSync(temporary, `${JSON.stringify(raw, null, 2)}\n`, { mode: 0o600, flag: "w" });
|
|
559
|
-
chmodSync(temporary, 0o600);
|
|
560
|
-
renameSync(temporary, path);
|
|
592
|
+
writeFileAtomicSync(path, `${JSON.stringify(raw, null, 2)}\n`);
|
|
561
593
|
return path;
|
|
562
594
|
}
|
|
563
595
|
/** Persists one top-level user setting without disturbing the rest of the file. */
|
package/dist/dedupe.d.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Repeated-run detection: a new tool result or message that pastes again a long block the agent already has in context
|
|
3
|
+
* (a relayed report that quotes every earlier turn) keeps only its new part. Exact line matches only, after trimming
|
|
4
|
+
* trailing spaces; near-duplicates are never collapsed. Deterministic; no requests.
|
|
5
|
+
*/
|
|
6
|
+
/** A run shorter than this, in lines or characters, is kept: a short repeat costs less than the pointer that replaces it. */
|
|
7
|
+
export declare const RUN_MIN_LINES = 20;
|
|
8
|
+
export declare const RUN_MIN_CHARS = 1500;
|
|
9
|
+
/** Errors and verdicts usually sit at the end of a result, so the last characters are never replaced. */
|
|
10
|
+
export declare const TAIL_KEEP_CHARS = 2000;
|
|
11
|
+
export interface RepeatedRun {
|
|
12
|
+
/** First line of the run in the new text, 0-based. */
|
|
13
|
+
start: number;
|
|
14
|
+
lines: number;
|
|
15
|
+
chars: number;
|
|
16
|
+
/** Where the earlier copy is, e.g. "an earlier bash result". */
|
|
17
|
+
where: string;
|
|
18
|
+
}
|
|
19
|
+
/** One text the agent has in context, with a stable identity so a sync adds it once. */
|
|
20
|
+
export interface SeenItem {
|
|
21
|
+
key: unknown;
|
|
22
|
+
where: string;
|
|
23
|
+
texts: string[];
|
|
24
|
+
}
|
|
25
|
+
export declare class SeenText {
|
|
26
|
+
/** Interned normalized lines; ids start at 1 so 0 marks a line never seen. */
|
|
27
|
+
private readonly lineIds;
|
|
28
|
+
private readonly docs;
|
|
29
|
+
private readonly windows;
|
|
30
|
+
private readonly keys;
|
|
31
|
+
clear(): void;
|
|
32
|
+
/** Match the index to what is in context now: a compaction or a move to another branch drops texts, so it rebuilds. */
|
|
33
|
+
sync(items: SeenItem[]): void;
|
|
34
|
+
add(text: string, where: string): void;
|
|
35
|
+
/** Runs of `text` that repeat one earlier text exactly, longest match first at each position, outside the kept tail. */
|
|
36
|
+
find(text: string): RepeatedRun[];
|
|
37
|
+
}
|
|
38
|
+
/** Replaces each run with one pointer line; `path` holds the full original text. */
|
|
39
|
+
export declare function collapseRuns(text: string, runs: RepeatedRun[], path: string): string;
|
|
40
|
+
/** The texts a session entry puts in context and where the agent saw them. Thinking, images, and summaries are not indexed. */
|
|
41
|
+
export declare function seenItem(entry: unknown): SeenItem | undefined;
|
package/dist/dedupe.js
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Repeated-run detection: a new tool result or message that pastes again a long block the agent already has in context
|
|
3
|
+
* (a relayed report that quotes every earlier turn) keeps only its new part. Exact line matches only, after trimming
|
|
4
|
+
* trailing spaces; near-duplicates are never collapsed. Deterministic; no requests.
|
|
5
|
+
*/
|
|
6
|
+
/** A run shorter than this, in lines or characters, is kept: a short repeat costs less than the pointer that replaces it. */
|
|
7
|
+
export const RUN_MIN_LINES = 20;
|
|
8
|
+
export const RUN_MIN_CHARS = 1500;
|
|
9
|
+
/** Errors and verdicts usually sit at the end of a result, so the last characters are never replaced. */
|
|
10
|
+
export const TAIL_KEEP_CHARS = 2000;
|
|
11
|
+
const BASE = 1_000_003;
|
|
12
|
+
/** BASE^(RUN_MIN_LINES - 1) mod 2^32, for the rolling window hash. */
|
|
13
|
+
const TOP = (() => { let power = 1; for (let index = 1; index < RUN_MIN_LINES; index++)
|
|
14
|
+
power = Math.imul(power, BASE); return power; })();
|
|
15
|
+
/** Several earlier copies of one window are enough to find the longest match; more only cost memory. */
|
|
16
|
+
const MAX_POSITIONS = 4;
|
|
17
|
+
/** Hash of every RUN_MIN_LINES-line window, by start line. */
|
|
18
|
+
function windowHashes(ids) {
|
|
19
|
+
const hashes = [];
|
|
20
|
+
if (ids.length < RUN_MIN_LINES)
|
|
21
|
+
return hashes;
|
|
22
|
+
let hash = 0;
|
|
23
|
+
for (let index = 0; index < RUN_MIN_LINES; index++)
|
|
24
|
+
hash = (Math.imul(hash, BASE) + ids[index]) | 0;
|
|
25
|
+
hashes.push(hash);
|
|
26
|
+
for (let index = RUN_MIN_LINES; index < ids.length; index++) {
|
|
27
|
+
hash = (Math.imul((hash - Math.imul(ids[index - RUN_MIN_LINES], TOP)) | 0, BASE) + ids[index]) | 0;
|
|
28
|
+
hashes.push(hash);
|
|
29
|
+
}
|
|
30
|
+
return hashes;
|
|
31
|
+
}
|
|
32
|
+
export class SeenText {
|
|
33
|
+
/** Interned normalized lines; ids start at 1 so 0 marks a line never seen. */
|
|
34
|
+
lineIds = new Map();
|
|
35
|
+
docs = [];
|
|
36
|
+
windows = new Map();
|
|
37
|
+
keys = new Set();
|
|
38
|
+
clear() {
|
|
39
|
+
this.lineIds.clear();
|
|
40
|
+
this.docs.length = 0;
|
|
41
|
+
this.windows.clear();
|
|
42
|
+
this.keys.clear();
|
|
43
|
+
}
|
|
44
|
+
/** Match the index to what is in context now: a compaction or a move to another branch drops texts, so it rebuilds. */
|
|
45
|
+
sync(items) {
|
|
46
|
+
const current = new Set(items.map(item => item.key));
|
|
47
|
+
for (const key of this.keys)
|
|
48
|
+
if (!current.has(key)) {
|
|
49
|
+
this.clear();
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
for (const item of items) {
|
|
53
|
+
if (this.keys.has(item.key))
|
|
54
|
+
continue;
|
|
55
|
+
this.keys.add(item.key);
|
|
56
|
+
for (const text of item.texts)
|
|
57
|
+
this.add(text, item.where);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
add(text, where) {
|
|
61
|
+
const lines = text.split("\n");
|
|
62
|
+
if (lines.length < RUN_MIN_LINES)
|
|
63
|
+
return;
|
|
64
|
+
const ids = lines.map(line => {
|
|
65
|
+
const normalized = line.trimEnd();
|
|
66
|
+
let id = this.lineIds.get(normalized);
|
|
67
|
+
if (id === undefined) {
|
|
68
|
+
id = this.lineIds.size + 1;
|
|
69
|
+
this.lineIds.set(normalized, id);
|
|
70
|
+
}
|
|
71
|
+
return id;
|
|
72
|
+
});
|
|
73
|
+
const doc = this.docs.push({ where, ids }) - 1;
|
|
74
|
+
windowHashes(ids).forEach((hash, line) => {
|
|
75
|
+
const positions = this.windows.get(hash);
|
|
76
|
+
if (!positions)
|
|
77
|
+
this.windows.set(hash, [[doc, line]]);
|
|
78
|
+
else if (positions.length < MAX_POSITIONS)
|
|
79
|
+
positions.push([doc, line]);
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
/** Runs of `text` that repeat one earlier text exactly, longest match first at each position, outside the kept tail. */
|
|
83
|
+
find(text) {
|
|
84
|
+
const lines = text.split("\n");
|
|
85
|
+
const cutoff = text.length - TAIL_KEEP_CHARS;
|
|
86
|
+
let limit = 0;
|
|
87
|
+
for (let offset = 0; limit < lines.length; limit++) {
|
|
88
|
+
const end = offset + lines[limit].length + 1;
|
|
89
|
+
if (end > cutoff)
|
|
90
|
+
break;
|
|
91
|
+
offset = end;
|
|
92
|
+
}
|
|
93
|
+
if (limit < RUN_MIN_LINES)
|
|
94
|
+
return [];
|
|
95
|
+
const ids = lines.slice(0, limit).map(line => this.lineIds.get(line.trimEnd()) ?? 0);
|
|
96
|
+
const hashes = windowHashes(ids);
|
|
97
|
+
const runs = [];
|
|
98
|
+
let index = 0;
|
|
99
|
+
while (index + RUN_MIN_LINES <= limit) {
|
|
100
|
+
let best;
|
|
101
|
+
for (const [doc, line] of this.windows.get(hashes[index]) ?? []) {
|
|
102
|
+
const seen = this.docs[doc].ids;
|
|
103
|
+
let length = 0;
|
|
104
|
+
while (index + length < limit && line + length < seen.length && ids[index + length] !== 0 && ids[index + length] === seen[line + length])
|
|
105
|
+
length++;
|
|
106
|
+
if (length >= RUN_MIN_LINES && (!best || length > best.length))
|
|
107
|
+
best = { doc, length };
|
|
108
|
+
}
|
|
109
|
+
if (best) {
|
|
110
|
+
let chars = 0;
|
|
111
|
+
for (let line = index; line < index + best.length; line++)
|
|
112
|
+
chars += lines[line].length + 1;
|
|
113
|
+
if (chars >= RUN_MIN_CHARS) {
|
|
114
|
+
runs.push({ start: index, lines: best.length, chars, where: this.docs[best.doc].where });
|
|
115
|
+
index += best.length;
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
index++;
|
|
120
|
+
}
|
|
121
|
+
return runs;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
/** Replaces each run with one pointer line; `path` holds the full original text. */
|
|
125
|
+
export function collapseRuns(text, runs, path) {
|
|
126
|
+
if (!runs.length)
|
|
127
|
+
return text;
|
|
128
|
+
const lines = text.split("\n");
|
|
129
|
+
const kept = [];
|
|
130
|
+
let index = 0;
|
|
131
|
+
for (const run of runs) {
|
|
132
|
+
kept.push(...lines.slice(index, run.start), `[pi-warden: the next ${run.lines} lines repeat ${run.where} — omitted; full text: ${path}]`);
|
|
133
|
+
index = run.start + run.lines;
|
|
134
|
+
}
|
|
135
|
+
kept.push(...lines.slice(index));
|
|
136
|
+
return kept.join("\n");
|
|
137
|
+
}
|
|
138
|
+
function textsOf(content) {
|
|
139
|
+
if (typeof content === "string")
|
|
140
|
+
return [content];
|
|
141
|
+
if (!Array.isArray(content))
|
|
142
|
+
return [];
|
|
143
|
+
return content.filter(part => part?.type === "text" && typeof part.text === "string").map(part => part.text);
|
|
144
|
+
}
|
|
145
|
+
/** The texts a session entry puts in context and where the agent saw them. Thinking, images, and summaries are not indexed. */
|
|
146
|
+
export function seenItem(entry) {
|
|
147
|
+
if (!entry || typeof entry !== "object")
|
|
148
|
+
return undefined;
|
|
149
|
+
const record = entry;
|
|
150
|
+
const key = typeof record.id === "string" ? record.id : entry;
|
|
151
|
+
if (record.type === "custom_message")
|
|
152
|
+
return { key, where: `an earlier ${String(record.customType ?? "custom")} message`, texts: textsOf(record.content) };
|
|
153
|
+
if (record.type !== "message" || !record.message)
|
|
154
|
+
return undefined;
|
|
155
|
+
const { role } = record.message;
|
|
156
|
+
const where = role === "toolResult" ? `an earlier ${String(record.message.toolName ?? "tool")} result`
|
|
157
|
+
: role === "user" ? "an earlier user message"
|
|
158
|
+
: role === "assistant" ? "an earlier assistant reply"
|
|
159
|
+
: role === "custom" ? `an earlier ${String(record.message.customType ?? "custom")} message`
|
|
160
|
+
: undefined;
|
|
161
|
+
return where ? { key, where, texts: textsOf(record.message.content) } : undefined;
|
|
162
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import type { Attempt } from "./stuck.js";
|
|
2
|
+
/** Byte cap for the whole `evidence` object; over it, the oldest runs go first, then the oldest edits. */
|
|
3
|
+
export declare const EVIDENCE_LIMIT = 4096;
|
|
4
|
+
/** A failed or check output parsed into failing tests, error lines, a location, a summary and an exit code. */
|
|
5
|
+
export type ParsedFailure = {
|
|
6
|
+
format: string;
|
|
7
|
+
failing: string[];
|
|
8
|
+
failing_more?: number;
|
|
9
|
+
errors: string[];
|
|
10
|
+
location?: string;
|
|
11
|
+
summary?: string;
|
|
12
|
+
exit?: number;
|
|
13
|
+
};
|
|
14
|
+
/** Parses a failed or check output into failing tests, error lines, a location, a summary and an exit code. */
|
|
15
|
+
export declare function parseFailure(text: string): ParsedFailure;
|
|
16
|
+
/**
|
|
17
|
+
* Normalises what changes between identical runs: durations, clock times and dates, addresses, long numbers, thread
|
|
18
|
+
* ids, temp paths, and line:column positions. Other digits stay, so "expected 3" and "expected 4" still differ.
|
|
19
|
+
*/
|
|
20
|
+
export declare function normaliseFailureText(value: string | undefined): string;
|
|
21
|
+
/**
|
|
22
|
+
* Order-free signature of a failure: which tests failed and with which errors, both normalised. Pass counts are left
|
|
23
|
+
* out, so a run narrowed by a flag (`-x`, a test filter) still matches the full run that failed the same way.
|
|
24
|
+
*/
|
|
25
|
+
export declare function failureSignature(parsed: ParsedFailure, raw?: string): string;
|
|
26
|
+
/** The edit pairs of an `edit` input, as `[oldText, newText]`: the `edits` array, or one top-level pair. */
|
|
27
|
+
export declare function editPairs(input: Record<string, unknown>): Array<[string, string]>;
|
|
28
|
+
/** Removed and added lines of one edit, as a multiset difference, so unchanged context lines drop out. */
|
|
29
|
+
export declare function lineDelta(oldText: string, newText: string): {
|
|
30
|
+
removed: string[];
|
|
31
|
+
added: string[];
|
|
32
|
+
};
|
|
33
|
+
/** What an `edit` or `write` changed: the path, the line counts, and a diff capped at `DIFF_CAP` characters. */
|
|
34
|
+
export type EditDiff = {
|
|
35
|
+
path: string;
|
|
36
|
+
added: number;
|
|
37
|
+
removed: number;
|
|
38
|
+
removedLines: string[];
|
|
39
|
+
addedLines: string[];
|
|
40
|
+
diff: string;
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* The change an `edit` or `write` made. Every string is as the agent wrote it: the caller redacts what leaves the
|
|
44
|
+
* machine. The diff is the whole added and removed text, not a line-by-line patch, because the judge compares one
|
|
45
|
+
* change against the next.
|
|
46
|
+
*/
|
|
47
|
+
export declare function editDiff(tool: string, input: Record<string, unknown>): EditDiff;
|
|
48
|
+
/** One run in the evidence: a failed call, or a call that ran a test, build or lint command and passed. */
|
|
49
|
+
export type EvidenceRun = {
|
|
50
|
+
n: number;
|
|
51
|
+
outcome: "failed" | "ok";
|
|
52
|
+
parser?: string;
|
|
53
|
+
failing_tests?: string[];
|
|
54
|
+
failing_tests_more?: number;
|
|
55
|
+
errors?: string[];
|
|
56
|
+
location?: string;
|
|
57
|
+
summary?: string;
|
|
58
|
+
output_tail?: string;
|
|
59
|
+
output_head_tail?: string;
|
|
60
|
+
/** The run number of the earlier run whose failure matches, after normalising times, temp paths and line numbers. */
|
|
61
|
+
same_failure_as_run?: number | null;
|
|
62
|
+
exit_code?: number;
|
|
63
|
+
};
|
|
64
|
+
/** One `edit`/`write` call in the evidence. */
|
|
65
|
+
export type EvidenceEdit = {
|
|
66
|
+
n: number;
|
|
67
|
+
path: string;
|
|
68
|
+
added_lines: number;
|
|
69
|
+
removed_lines: number;
|
|
70
|
+
diff: string;
|
|
71
|
+
};
|
|
72
|
+
/** The counts a judge can weigh without reading every run. */
|
|
73
|
+
export type EvidenceDigest = {
|
|
74
|
+
failed_runs: number;
|
|
75
|
+
distinct_failures: number;
|
|
76
|
+
/** Runs whose command repeats an earlier run's command, whatever the outcome. */
|
|
77
|
+
same_command_runs: number[];
|
|
78
|
+
latest_failure_seen_before: boolean;
|
|
79
|
+
edits_between_failed_runs: number;
|
|
80
|
+
information_calls_between_failed_runs: number;
|
|
81
|
+
};
|
|
82
|
+
export type StuckEvidence = {
|
|
83
|
+
runs: EvidenceRun[];
|
|
84
|
+
edits: EvidenceEdit[];
|
|
85
|
+
digest: EvidenceDigest;
|
|
86
|
+
};
|
|
87
|
+
/**
|
|
88
|
+
* Builds the evidence for one stuck window: a run per failed call and per passing check command, an edit per
|
|
89
|
+
* `edit`/`write`, and a digest. The oldest runs are dropped first when the whole object is over `EVIDENCE_LIMIT`
|
|
90
|
+
* bytes, then the oldest edits; the digest is then recomputed over what is left, and a run that pointed at a dropped
|
|
91
|
+
* run points at nothing. The floor is one run plus the digest, which is far under the cap.
|
|
92
|
+
*/
|
|
93
|
+
export declare function buildStuckEvidence(attempts: readonly Attempt[], limit?: number): StuckEvidence;
|