pi-readseek 0.3.2 → 0.3.3
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 +24 -18
- package/index.ts +16 -136
- package/package.json +1 -1
- package/src/edit-output.ts +1 -7
- package/src/edit.ts +3 -14
- package/src/grep-output.ts +0 -26
- package/src/grep.ts +1 -13
- package/src/read-output.ts +1 -27
- package/src/read.ts +1 -10
- package/src/readseek-client.ts +1 -1
- package/src/sg-output.ts +0 -30
- package/src/sg.ts +14 -22
- package/src/write.ts +1 -13
- package/src/context-application.ts +0 -70
- package/src/context-hygiene.ts +0 -512
- package/src/doom-loop-suggestions.ts +0 -42
- package/src/doom-loop.ts +0 -216
- package/src/find.ts +0 -613
- package/src/ls.ts +0 -293
- package/src/readseek-command.ts +0 -155
- package/src/readseek-repo.ts +0 -50
package/src/doom-loop.ts
DELETED
|
@@ -1,216 +0,0 @@
|
|
|
1
|
-
import { SUGGESTIONS, GENERIC_SUGGESTION } from "./doom-loop-suggestions.js";
|
|
2
|
-
export const MAX_RECENT_TOOL_CALLS = 24;
|
|
3
|
-
|
|
4
|
-
export interface RecordedToolCall {
|
|
5
|
-
toolCallId: string;
|
|
6
|
-
toolName: string;
|
|
7
|
-
input: Record<string, unknown>;
|
|
8
|
-
fingerprint: string;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export interface DoomLoopStep {
|
|
12
|
-
toolName: string;
|
|
13
|
-
input: Record<string, unknown>;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export type DoomLoopWarning =
|
|
17
|
-
| {
|
|
18
|
-
kind: "identical-tail";
|
|
19
|
-
toolName: string;
|
|
20
|
-
fingerprint: string;
|
|
21
|
-
}
|
|
22
|
-
| {
|
|
23
|
-
kind: "repeated-subsequence";
|
|
24
|
-
toolName: string;
|
|
25
|
-
fingerprint: string;
|
|
26
|
-
steps: DoomLoopStep[];
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
export interface DoomLoopState {
|
|
30
|
-
recentCalls: RecordedToolCall[];
|
|
31
|
-
pendingWarnings: Map<string, DoomLoopWarning>;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export function createDoomLoopState(): DoomLoopState {
|
|
35
|
-
return {
|
|
36
|
-
recentCalls: [],
|
|
37
|
-
pendingWarnings: new Map<string, DoomLoopWarning>(),
|
|
38
|
-
};
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function stableStringify(value: unknown): string {
|
|
42
|
-
if (Array.isArray(value)) {
|
|
43
|
-
return `[${value.map((item) => stableStringify(item)).join(",")}]`;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
if (value && typeof value === "object") {
|
|
47
|
-
const entries = Object.entries(value as Record<string, unknown>).sort(([a], [b]) => a.localeCompare(b));
|
|
48
|
-
return `{${entries.map(([key, entryValue]) => `${JSON.stringify(key)}:${stableStringify(entryValue)}`).join(",")}}`;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
return JSON.stringify(value);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export function makeToolFingerprint(toolName: string, input: Record<string, unknown>): string {
|
|
55
|
-
return `${toolName}:${stableStringify(input)}`;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function sameFingerprints(left: RecordedToolCall[], right: RecordedToolCall[]): boolean {
|
|
59
|
-
return left.length === right.length && left.every((call, index) => call.fingerprint === right[index]?.fingerprint);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function hasIdenticalTail(calls: RecordedToolCall[]): boolean {
|
|
63
|
-
if (calls.length < 3) {
|
|
64
|
-
return false;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
const last = calls[calls.length - 1]?.fingerprint;
|
|
68
|
-
return calls[calls.length - 2]?.fingerprint === last && calls[calls.length - 3]?.fingerprint === last;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function findRepeatedSubsequenceWindow(calls: RecordedToolCall[]): number | null {
|
|
72
|
-
const maxWindowSize = Math.floor(calls.length / 3);
|
|
73
|
-
for (let windowSize = 2; windowSize <= maxWindowSize; windowSize++) {
|
|
74
|
-
const newest = calls.slice(-windowSize);
|
|
75
|
-
const middle = calls.slice(-windowSize * 2, -windowSize);
|
|
76
|
-
const oldest = calls.slice(-windowSize * 3, -windowSize * 2);
|
|
77
|
-
if (sameFingerprints(newest, middle) && sameFingerprints(middle, oldest)) {
|
|
78
|
-
return windowSize;
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
return null;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
export function recordToolCall(
|
|
85
|
-
state: DoomLoopState,
|
|
86
|
-
toolName: string,
|
|
87
|
-
toolCallId: string,
|
|
88
|
-
input: Record<string, unknown>,
|
|
89
|
-
): void {
|
|
90
|
-
const fingerprint = makeToolFingerprint(toolName, input);
|
|
91
|
-
state.recentCalls.push({ toolCallId, toolName, input, fingerprint });
|
|
92
|
-
|
|
93
|
-
if (state.recentCalls.length > MAX_RECENT_TOOL_CALLS) {
|
|
94
|
-
state.recentCalls.splice(0, state.recentCalls.length - MAX_RECENT_TOOL_CALLS);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
if (hasIdenticalTail(state.recentCalls)) {
|
|
98
|
-
state.pendingWarnings.set(toolCallId, {
|
|
99
|
-
kind: "identical-tail",
|
|
100
|
-
toolName,
|
|
101
|
-
fingerprint,
|
|
102
|
-
});
|
|
103
|
-
return;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
const windowSize = findRepeatedSubsequenceWindow(state.recentCalls);
|
|
107
|
-
if (windowSize !== null) {
|
|
108
|
-
const newest = state.recentCalls.slice(-windowSize);
|
|
109
|
-
state.pendingWarnings.set(toolCallId, {
|
|
110
|
-
kind: "repeated-subsequence",
|
|
111
|
-
toolName,
|
|
112
|
-
fingerprint,
|
|
113
|
-
steps: newest.map((call) => ({ toolName: call.toolName, input: call.input })),
|
|
114
|
-
});
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
export function consumeDoomLoopWarning(
|
|
119
|
-
state: DoomLoopState,
|
|
120
|
-
toolCallId: string,
|
|
121
|
-
): DoomLoopWarning | null {
|
|
122
|
-
const warning = state.pendingWarnings.get(toolCallId);
|
|
123
|
-
if (!warning) return null;
|
|
124
|
-
state.pendingWarnings.delete(toolCallId);
|
|
125
|
-
return warning;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
const COMPACT_LINE_BUDGET = 80;
|
|
129
|
-
const STEP_PREFIX = " → ";
|
|
130
|
-
|
|
131
|
-
function truncate(value: string, max: number): string {
|
|
132
|
-
if (value.length <= max) return value;
|
|
133
|
-
if (max <= 1) return "…";
|
|
134
|
-
return `${value.slice(0, max - 1)}…`;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
function renderCompactStep(toolName: string, input: Record<string, unknown>): string {
|
|
138
|
-
const keys = Object.keys(input).sort();
|
|
139
|
-
const salient = keys.slice(0, 2);
|
|
140
|
-
const base = `${STEP_PREFIX}${toolName}`;
|
|
141
|
-
if (salient.length === 0) return truncate(`${base} {}`, COMPACT_LINE_BUDGET);
|
|
142
|
-
|
|
143
|
-
let line = base;
|
|
144
|
-
for (const key of salient) {
|
|
145
|
-
const rendered = JSON.stringify(input[key]);
|
|
146
|
-
const part = ` ${key}=${rendered}`;
|
|
147
|
-
const candidate = line + part;
|
|
148
|
-
if (candidate.length > COMPACT_LINE_BUDGET) {
|
|
149
|
-
const remaining = COMPACT_LINE_BUDGET - (line + ` ${key}=`).length;
|
|
150
|
-
line = `${line} ${key}=${truncate(rendered, Math.max(1, remaining))}`;
|
|
151
|
-
return line;
|
|
152
|
-
}
|
|
153
|
-
line = candidate;
|
|
154
|
-
}
|
|
155
|
-
return truncate(line, COMPACT_LINE_BUDGET);
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
function parseFingerprintInput(fingerprint: string): Record<string, unknown> {
|
|
159
|
-
const colon = fingerprint.indexOf(":");
|
|
160
|
-
if (colon < 0) return {};
|
|
161
|
-
const json = fingerprint.slice(colon + 1);
|
|
162
|
-
try {
|
|
163
|
-
const parsed = JSON.parse(json);
|
|
164
|
-
return parsed && typeof parsed === "object" ? (parsed as Record<string, unknown>) : {};
|
|
165
|
-
} catch {
|
|
166
|
-
return {};
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
function suggestionsFor(toolName: string): string[] {
|
|
171
|
-
const entry = SUGGESTIONS[toolName];
|
|
172
|
-
if (entry && entry.length > 0) return [...entry];
|
|
173
|
-
return [GENERIC_SUGGESTION];
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
function renderSuggestionBullets(toolNames: string[]): string {
|
|
177
|
-
const seen = new Set<string>();
|
|
178
|
-
const lines: string[] = [];
|
|
179
|
-
for (const name of toolNames) {
|
|
180
|
-
if (seen.has(name)) continue;
|
|
181
|
-
seen.add(name);
|
|
182
|
-
const bullets = suggestionsFor(name);
|
|
183
|
-
lines.push(`For ${name}:`);
|
|
184
|
-
for (const bullet of bullets) {
|
|
185
|
-
lines.push(` • ${bullet}`);
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
return lines.join("\n");
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
export function formatDoomLoopMessage(warning: DoomLoopWarning): string {
|
|
192
|
-
if (warning.kind === "identical-tail") {
|
|
193
|
-
const input = parseFingerprintInput(warning.fingerprint);
|
|
194
|
-
const compact = renderCompactStep(warning.toolName, input);
|
|
195
|
-
const suggestions = renderSuggestionBullets([warning.toolName]);
|
|
196
|
-
return [
|
|
197
|
-
"⚠ REPEATED-CALL WARNING: This is the 3rd identical tool call.",
|
|
198
|
-
compact,
|
|
199
|
-
"",
|
|
200
|
-
"Continuing this pattern will not make progress. Suggestions:",
|
|
201
|
-
suggestions,
|
|
202
|
-
].join("\n");
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
const stepLines = warning.steps.map((step) => renderCompactStep(step.toolName, step.input));
|
|
206
|
-
const suggestions = renderSuggestionBullets(warning.steps.map((step) => step.toolName));
|
|
207
|
-
return [
|
|
208
|
-
`⚠ ALTERNATING-CALL WARNING: You have called this sequence ${warning.steps.length > 0 ? "3 times" : ""}:`.trimEnd(),
|
|
209
|
-
...stepLines,
|
|
210
|
-
"",
|
|
211
|
-
"Neither call is producing new information. Break the loop with a different approach.",
|
|
212
|
-
"",
|
|
213
|
-
suggestions,
|
|
214
|
-
].join("\n");
|
|
215
|
-
}
|
|
216
|
-
|