@possumtech/rummy 0.2.8 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +13 -2
- package/EXCEPTIONS.md +46 -0
- package/PLUGINS.md +422 -188
- package/SPEC.md +440 -106
- package/migrations/001_initial_schema.sql +5 -3
- package/package.json +17 -5
- package/service.js +5 -3
- package/src/agent/AgentLoop.js +252 -55
- package/src/agent/ContextAssembler.js +20 -4
- package/src/agent/KnownStore.js +82 -25
- package/src/agent/ProjectAgent.js +4 -1
- package/src/agent/ResponseHealer.js +86 -32
- package/src/agent/TurnExecutor.js +542 -207
- package/src/agent/XmlParser.js +77 -41
- package/src/agent/known_store.sql +68 -4
- package/src/agent/schemes.sql +3 -0
- package/src/agent/tokens.js +7 -21
- package/src/agent/turns.sql +15 -1
- package/src/hooks/HookRegistry.js +7 -0
- package/src/hooks/Hooks.js +15 -0
- package/src/hooks/PluginContext.js +14 -1
- package/src/hooks/RummyContext.js +16 -4
- package/src/hooks/ToolRegistry.js +77 -19
- package/src/llm/LlmProvider.js +27 -8
- package/src/llm/OpenAiClient.js +20 -0
- package/src/llm/OpenRouterClient.js +24 -2
- package/src/llm/XaiClient.js +47 -2
- package/src/plugins/ask_user/README.md +4 -4
- package/src/plugins/ask_user/ask_user.js +5 -5
- package/src/plugins/ask_user/ask_userDoc.js +29 -0
- package/src/plugins/budget/README.md +31 -0
- package/src/plugins/budget/budget.js +55 -0
- package/src/plugins/cp/README.md +5 -4
- package/src/plugins/cp/cp.js +10 -6
- package/src/plugins/cp/cpDoc.js +29 -0
- package/src/plugins/engine/engine.sql +1 -8
- package/src/plugins/engine/turn_context.sql +4 -9
- package/src/plugins/env/README.md +3 -4
- package/src/plugins/env/env.js +5 -5
- package/src/plugins/env/envDoc.js +29 -0
- package/src/plugins/file/README.md +9 -12
- package/src/plugins/file/file.js +34 -35
- package/src/plugins/get/README.md +2 -2
- package/src/plugins/get/get.js +77 -6
- package/src/plugins/get/getDoc.js +51 -0
- package/src/plugins/hedberg/hedberg.js +2 -1
- package/src/plugins/hedberg/matcher.js +10 -29
- package/src/plugins/hedberg/normalize.js +28 -0
- package/src/plugins/hedberg/patterns.js +25 -27
- package/src/plugins/hedberg/sed.js +17 -10
- package/src/plugins/index.js +66 -14
- package/src/plugins/instructions/README.md +6 -2
- package/src/plugins/instructions/instructions.js +20 -4
- package/src/plugins/instructions/preamble.md +19 -5
- package/src/plugins/known/README.md +10 -7
- package/src/plugins/known/known.js +23 -17
- package/src/plugins/known/knownDoc.js +34 -0
- package/src/plugins/mv/README.md +5 -4
- package/src/plugins/mv/mv.js +27 -6
- package/src/plugins/mv/mvDoc.js +45 -0
- package/src/plugins/performed/README.md +15 -0
- package/src/plugins/performed/performed.js +45 -0
- package/src/plugins/persona/persona.js +78 -0
- package/src/plugins/previous/README.md +3 -2
- package/src/plugins/previous/previous.js +33 -24
- package/src/plugins/progress/README.md +1 -2
- package/src/plugins/progress/progress.js +33 -21
- package/src/plugins/prompt/README.md +5 -5
- package/src/plugins/prompt/prompt.js +15 -17
- package/src/plugins/rm/README.md +4 -4
- package/src/plugins/rm/rm.js +32 -20
- package/src/plugins/rm/rmDoc.js +30 -0
- package/src/plugins/rpc/README.md +15 -28
- package/src/plugins/rpc/rpc.js +42 -77
- package/src/plugins/set/README.md +13 -12
- package/src/plugins/set/set.js +107 -16
- package/src/plugins/set/setDoc.js +49 -0
- package/src/plugins/sh/README.md +4 -4
- package/src/plugins/sh/sh.js +5 -5
- package/src/plugins/sh/shDoc.js +29 -0
- package/src/plugins/{skills/skills.js → skill/skill.js} +10 -51
- package/src/plugins/summarize/README.md +6 -5
- package/src/plugins/summarize/summarize.js +7 -6
- package/src/plugins/summarize/summarizeDoc.js +33 -0
- package/src/plugins/telemetry/telemetry.js +16 -9
- package/src/plugins/think/README.md +20 -0
- package/src/plugins/think/think.js +5 -0
- package/src/plugins/unknown/README.md +6 -5
- package/src/plugins/unknown/unknown.js +12 -9
- package/src/plugins/unknown/unknownDoc.js +31 -0
- package/src/plugins/update/README.md +3 -8
- package/src/plugins/update/update.js +7 -6
- package/src/plugins/update/updateDoc.js +33 -0
- package/src/server/ClientConnection.js +59 -45
- package/src/server/RpcRegistry.js +52 -4
- package/src/sql/v_model_context.sql +10 -25
- package/src/plugins/ask_user/docs.md +0 -2
- package/src/plugins/cp/docs.md +0 -2
- package/src/plugins/current/README.md +0 -14
- package/src/plugins/current/current.js +0 -47
- package/src/plugins/env/docs.md +0 -4
- package/src/plugins/get/docs.md +0 -10
- package/src/plugins/known/docs.md +0 -3
- package/src/plugins/mv/docs.md +0 -2
- package/src/plugins/rm/docs.md +0 -6
- package/src/plugins/set/docs.md +0 -6
- package/src/plugins/sh/docs.md +0 -2
- package/src/plugins/skills/README.md +0 -25
- package/src/plugins/store/README.md +0 -20
- package/src/plugins/store/docs.md +0 -6
- package/src/plugins/store/store.js +0 -63
- package/src/plugins/summarize/docs.md +0 -4
- package/src/plugins/unknown/docs.md +0 -5
- package/src/plugins/update/docs.md +0 -4
package/src/agent/KnownStore.js
CHANGED
|
@@ -2,9 +2,25 @@ import slugify from "../sql/functions/slugify.js";
|
|
|
2
2
|
|
|
3
3
|
export default class KnownStore {
|
|
4
4
|
#db;
|
|
5
|
+
#onChanged;
|
|
6
|
+
#schemes = new Map();
|
|
7
|
+
#seq = 0;
|
|
5
8
|
|
|
6
|
-
constructor(db) {
|
|
9
|
+
constructor(db, { onChanged } = {}) {
|
|
7
10
|
this.#db = db;
|
|
11
|
+
this.#onChanged = onChanged || null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async loadSchemes(db) {
|
|
15
|
+
const rows = await (db || this.#db).get_all_schemes.all();
|
|
16
|
+
this.#schemes.clear();
|
|
17
|
+
for (const row of rows) {
|
|
18
|
+
this.#schemes.set(row.name, row);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
#emitChanged(runId, path, changeType) {
|
|
23
|
+
if (this.#onChanged) this.#onChanged({ runId, path, changeType });
|
|
8
24
|
}
|
|
9
25
|
|
|
10
26
|
static scheme(path) {
|
|
@@ -14,15 +30,16 @@ export default class KnownStore {
|
|
|
14
30
|
|
|
15
31
|
static normalizePath(path) {
|
|
16
32
|
if (!path?.includes("://")) return path;
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
33
|
+
const sep = path.indexOf("://");
|
|
34
|
+
const scheme = path.slice(0, sep).toLowerCase();
|
|
35
|
+
const rest = path.slice(sep + 3);
|
|
36
|
+
try {
|
|
37
|
+
// Decode first (idempotent), then encode — but preserve slashes
|
|
38
|
+
const decoded = decodeURIComponent(rest);
|
|
39
|
+
return `${scheme}://${decoded.split("/").map(encodeURIComponent).join("/")}`;
|
|
40
|
+
} catch {
|
|
41
|
+
return `${scheme}://${rest.split("/").map(encodeURIComponent).join("/")}`;
|
|
42
|
+
}
|
|
26
43
|
}
|
|
27
44
|
|
|
28
45
|
async nextTurn(runId) {
|
|
@@ -30,21 +47,24 @@ export default class KnownStore {
|
|
|
30
47
|
return row.turn;
|
|
31
48
|
}
|
|
32
49
|
|
|
33
|
-
async dedup(runId, scheme, target) {
|
|
34
|
-
const
|
|
50
|
+
async dedup(runId, scheme, target, turn) {
|
|
51
|
+
const encodedTarget = encodeURIComponent(target);
|
|
52
|
+
const turnPrefix = turn ? `turn_${turn}/` : "";
|
|
53
|
+
const candidate = `${scheme}://${turnPrefix}${encodedTarget}`;
|
|
35
54
|
const existing = await this.#db.get_entry_body.get({
|
|
36
55
|
run_id: runId,
|
|
37
|
-
path:
|
|
56
|
+
path: candidate,
|
|
38
57
|
});
|
|
39
58
|
if (!existing) return candidate;
|
|
40
|
-
return `${candidate}_${
|
|
59
|
+
return `${candidate}_${++this.#seq}`;
|
|
41
60
|
}
|
|
42
61
|
|
|
43
|
-
async slugPath(runId, scheme, content) {
|
|
44
|
-
const
|
|
62
|
+
async slugPath(runId, scheme, content, summary) {
|
|
63
|
+
const source = summary || content || "";
|
|
64
|
+
const base = slugify(source);
|
|
45
65
|
const prefix = `${scheme}://`;
|
|
46
66
|
|
|
47
|
-
if (!base) return `${prefix}${
|
|
67
|
+
if (!base) return `${prefix}${++this.#seq}`;
|
|
48
68
|
|
|
49
69
|
const candidate = `${prefix}${base}`;
|
|
50
70
|
const existing = await this.#db.get_entry_body.get({
|
|
@@ -53,7 +73,7 @@ export default class KnownStore {
|
|
|
53
73
|
});
|
|
54
74
|
if (!existing) return candidate;
|
|
55
75
|
|
|
56
|
-
return `${prefix}${base}_${
|
|
76
|
+
return `${prefix}${base}_${++this.#seq}`;
|
|
57
77
|
}
|
|
58
78
|
|
|
59
79
|
async upsert(
|
|
@@ -70,11 +90,12 @@ export default class KnownStore {
|
|
|
70
90
|
loopId = null,
|
|
71
91
|
} = {},
|
|
72
92
|
) {
|
|
93
|
+
const normalized = KnownStore.normalizePath(path);
|
|
73
94
|
await this.#db.upsert_known_entry.run({
|
|
74
95
|
run_id: runId,
|
|
75
96
|
loop_id: loopId,
|
|
76
97
|
turn,
|
|
77
|
-
path:
|
|
98
|
+
path: normalized,
|
|
78
99
|
body,
|
|
79
100
|
status,
|
|
80
101
|
fidelity,
|
|
@@ -82,14 +103,17 @@ export default class KnownStore {
|
|
|
82
103
|
attributes: attributes ? JSON.stringify(attributes) : null,
|
|
83
104
|
updated_at: updatedAt,
|
|
84
105
|
});
|
|
106
|
+
this.#emitChanged(runId, normalized, "upsert");
|
|
85
107
|
}
|
|
86
108
|
|
|
87
109
|
async promote(runId, path, turn) {
|
|
110
|
+
const normalized = KnownStore.normalizePath(path);
|
|
88
111
|
await this.#db.promote_path.run({
|
|
89
112
|
run_id: runId,
|
|
90
|
-
path:
|
|
113
|
+
path: normalized,
|
|
91
114
|
turn,
|
|
92
115
|
});
|
|
116
|
+
this.#emitChanged(runId, normalized, "promote");
|
|
93
117
|
}
|
|
94
118
|
|
|
95
119
|
async setFileFidelity(runId, pattern, fidelity) {
|
|
@@ -101,28 +125,35 @@ export default class KnownStore {
|
|
|
101
125
|
if (result.changes === 0) {
|
|
102
126
|
await this.upsert(runId, 0, pattern, "", 200, { fidelity });
|
|
103
127
|
}
|
|
128
|
+
this.#emitChanged(runId, pattern, "fidelity");
|
|
104
129
|
}
|
|
105
130
|
|
|
106
131
|
async setFidelity(runId, path, fidelity) {
|
|
132
|
+
const normalized = KnownStore.normalizePath(path);
|
|
107
133
|
await this.#db.set_fidelity.run({
|
|
108
134
|
run_id: runId,
|
|
109
|
-
path:
|
|
135
|
+
path: normalized,
|
|
110
136
|
fidelity,
|
|
111
137
|
});
|
|
138
|
+
this.#emitChanged(runId, normalized, "fidelity");
|
|
112
139
|
}
|
|
113
140
|
|
|
114
141
|
async demote(runId, path) {
|
|
142
|
+
const normalized = KnownStore.normalizePath(path);
|
|
115
143
|
await this.#db.demote_path.run({
|
|
116
144
|
run_id: runId,
|
|
117
|
-
path:
|
|
145
|
+
path: normalized,
|
|
118
146
|
});
|
|
147
|
+
this.#emitChanged(runId, normalized, "demote");
|
|
119
148
|
}
|
|
120
149
|
|
|
121
150
|
async remove(runId, path) {
|
|
151
|
+
const normalized = KnownStore.normalizePath(path);
|
|
122
152
|
await this.#db.delete_known_entry.run({
|
|
123
153
|
run_id: runId,
|
|
124
|
-
path:
|
|
154
|
+
path: normalized,
|
|
125
155
|
});
|
|
156
|
+
this.#emitChanged(runId, normalized, "remove");
|
|
126
157
|
}
|
|
127
158
|
|
|
128
159
|
async removeFilesByPattern(runId, pattern) {
|
|
@@ -130,6 +161,7 @@ export default class KnownStore {
|
|
|
130
161
|
run_id: runId,
|
|
131
162
|
pattern,
|
|
132
163
|
});
|
|
164
|
+
this.#emitChanged(runId, pattern, "remove");
|
|
133
165
|
}
|
|
134
166
|
|
|
135
167
|
static #bodyPattern(body) {
|
|
@@ -143,6 +175,7 @@ export default class KnownStore {
|
|
|
143
175
|
body: KnownStore.#bodyPattern(body),
|
|
144
176
|
turn,
|
|
145
177
|
});
|
|
178
|
+
this.#emitChanged(runId, path, "promote");
|
|
146
179
|
}
|
|
147
180
|
|
|
148
181
|
async demoteByPattern(runId, path, body) {
|
|
@@ -151,6 +184,7 @@ export default class KnownStore {
|
|
|
151
184
|
path,
|
|
152
185
|
body: KnownStore.#bodyPattern(body),
|
|
153
186
|
});
|
|
187
|
+
this.#emitChanged(runId, path, "demote");
|
|
154
188
|
}
|
|
155
189
|
|
|
156
190
|
async getEntriesByPattern(runId, path, body, { limit, offset } = {}) {
|
|
@@ -169,6 +203,7 @@ export default class KnownStore {
|
|
|
169
203
|
path,
|
|
170
204
|
body: KnownStore.#bodyPattern(body),
|
|
171
205
|
});
|
|
206
|
+
this.#emitChanged(runId, path, "remove");
|
|
172
207
|
}
|
|
173
208
|
|
|
174
209
|
async updateBodyByPattern(runId, path, body, newBody) {
|
|
@@ -178,21 +213,41 @@ export default class KnownStore {
|
|
|
178
213
|
body: KnownStore.#bodyPattern(body),
|
|
179
214
|
new_body: newBody,
|
|
180
215
|
});
|
|
216
|
+
this.#emitChanged(runId, path, "body");
|
|
181
217
|
}
|
|
182
218
|
|
|
183
219
|
async resolve(runId, path, status, body) {
|
|
220
|
+
const normalized = KnownStore.normalizePath(path);
|
|
184
221
|
await this.#db.resolve_known_entry.run({
|
|
185
222
|
run_id: runId,
|
|
186
|
-
path:
|
|
223
|
+
path: normalized,
|
|
187
224
|
status,
|
|
188
225
|
body,
|
|
189
226
|
});
|
|
227
|
+
this.#emitChanged(runId, normalized, "resolve");
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
async restoreSummarizedPrompts(runId) {
|
|
231
|
+
await this.#db.restore_summarized_prompts.run({ run_id: runId });
|
|
232
|
+
this.#emitChanged(runId, "prompt://batch", "fidelity");
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async demotePreviousLoopLogging(runId, loopId) {
|
|
236
|
+
await this.#db.demote_previous_loop_logging.run({
|
|
237
|
+
run_id: runId,
|
|
238
|
+
loop_id: loopId,
|
|
239
|
+
});
|
|
240
|
+
this.#emitChanged(runId, "logging://batch", "fidelity");
|
|
190
241
|
}
|
|
191
242
|
|
|
192
243
|
async getLog(runId) {
|
|
193
244
|
return this.#db.get_results.all({ run_id: runId });
|
|
194
245
|
}
|
|
195
246
|
|
|
247
|
+
async getEntries(runId) {
|
|
248
|
+
return this.#db.get_known_entries.all({ run_id: runId });
|
|
249
|
+
}
|
|
250
|
+
|
|
196
251
|
async getFileEntries(runId) {
|
|
197
252
|
return this.#db.get_file_entries.all({ run_id: runId });
|
|
198
253
|
}
|
|
@@ -237,11 +292,13 @@ export default class KnownStore {
|
|
|
237
292
|
}
|
|
238
293
|
|
|
239
294
|
async setAttributes(runId, path, attrs) {
|
|
295
|
+
const normalized = KnownStore.normalizePath(path);
|
|
240
296
|
await this.#db.update_entry_attributes.run({
|
|
241
297
|
run_id: runId,
|
|
242
|
-
path:
|
|
298
|
+
path: normalized,
|
|
243
299
|
attributes: JSON.stringify(attrs),
|
|
244
300
|
});
|
|
301
|
+
this.#emitChanged(runId, normalized, "attributes");
|
|
245
302
|
}
|
|
246
303
|
|
|
247
304
|
async getState(runId, path) {
|
|
@@ -14,7 +14,10 @@ export default class ProjectAgent {
|
|
|
14
14
|
this.#db = db;
|
|
15
15
|
this.#hooks = hooks;
|
|
16
16
|
this.#llm = new LlmProvider(db);
|
|
17
|
-
this.#knownStore = new KnownStore(db
|
|
17
|
+
this.#knownStore = new KnownStore(db, {
|
|
18
|
+
onChanged: (event) => hooks.entry.changed.emit(event).catch(() => {}),
|
|
19
|
+
});
|
|
20
|
+
this.#knownStore.loadSchemes(db);
|
|
18
21
|
|
|
19
22
|
const turnExecutor = new TurnExecutor(
|
|
20
23
|
db,
|
|
@@ -1,10 +1,57 @@
|
|
|
1
1
|
const MAX_STALLS = Number(process.env.RUMMY_MAX_STALLS) || 3;
|
|
2
|
-
const
|
|
2
|
+
const MIN_CYCLES = Number(process.env.RUMMY_MIN_CYCLES) || 3;
|
|
3
|
+
const MAX_CYCLE_PERIOD = Number(process.env.RUMMY_MAX_CYCLE_PERIOD) || 4;
|
|
4
|
+
const MAX_UPDATE_REPEATS = Number(process.env.RUMMY_MAX_UPDATE_REPEATS) || 3;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Build a stable fingerprint for a single recorded entry.
|
|
8
|
+
* Uses scheme + original command target + all op-defining attributes.
|
|
9
|
+
* Excludes body (content too granular; same operation ≠ same content).
|
|
10
|
+
*/
|
|
11
|
+
function cmdFingerprint(entry) {
|
|
12
|
+
const attrs = { ...(entry.attributes ?? {}) };
|
|
13
|
+
delete attrs.body;
|
|
14
|
+
const target =
|
|
15
|
+
attrs.path ?? attrs.command ?? attrs.query ?? attrs.question ?? "";
|
|
16
|
+
delete attrs.path;
|
|
17
|
+
const extra = Object.keys(attrs)
|
|
18
|
+
.toSorted()
|
|
19
|
+
.filter((k) => attrs[k] != null)
|
|
20
|
+
.map((k) => `${k}=${attrs[k]}`)
|
|
21
|
+
.join(",");
|
|
22
|
+
return `${entry.scheme}:${target}${extra ? `[${extra}]` : ""}`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Detect a repeating cycle in the fingerprint history.
|
|
27
|
+
* Checks periods 1..MAX_CYCLE_PERIOD for MIN_CYCLES consecutive repetitions.
|
|
28
|
+
* Catches AAAA (period 1), ABABAB (period 2), ABCABCABC (period 3), etc.
|
|
29
|
+
*/
|
|
30
|
+
function detectCycle(history) {
|
|
31
|
+
for (let k = 1; k <= MAX_CYCLE_PERIOD; k++) {
|
|
32
|
+
const needed = k * MIN_CYCLES;
|
|
33
|
+
if (history.length < needed) continue;
|
|
34
|
+
const tail = history.slice(-needed);
|
|
35
|
+
const cycle = tail.slice(0, k);
|
|
36
|
+
let match = true;
|
|
37
|
+
outer: for (let rep = 0; rep < MIN_CYCLES; rep++) {
|
|
38
|
+
for (let j = 0; j < k; j++) {
|
|
39
|
+
if (tail[rep * k + j] !== cycle[j]) {
|
|
40
|
+
match = false;
|
|
41
|
+
break outer;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if (match) return { detected: true, period: k, cycles: MIN_CYCLES };
|
|
46
|
+
}
|
|
47
|
+
return { detected: false };
|
|
48
|
+
}
|
|
3
49
|
|
|
4
50
|
export default class ResponseHealer {
|
|
5
51
|
#stallCount = 0;
|
|
6
|
-
#
|
|
7
|
-
#
|
|
52
|
+
#turnHistory = [];
|
|
53
|
+
#lastUpdateText = null;
|
|
54
|
+
#updateRepeatCount = 0;
|
|
8
55
|
|
|
9
56
|
/**
|
|
10
57
|
* Heal a missing status tag. Called when the model emits
|
|
@@ -49,38 +96,28 @@ export default class ResponseHealer {
|
|
|
49
96
|
}
|
|
50
97
|
|
|
51
98
|
/**
|
|
52
|
-
*
|
|
99
|
+
* Detect cyclic tool patterns across turns.
|
|
53
100
|
* Returns { continue: boolean, reason?: string }
|
|
54
101
|
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
102
|
+
* Appends this turn's fingerprint to history, then checks whether the
|
|
103
|
+
* history ends in a repeating cycle of period 1..MAX_CYCLE_PERIOD with
|
|
104
|
+
* at least MIN_CYCLES consecutive repetitions.
|
|
105
|
+
*
|
|
106
|
+
* Catches AAAA (period 1), ABABAB (period 2), ABCABC (period 3), etc.
|
|
107
|
+
* Turns with no tool calls are skipped — they don't contribute to a cycle.
|
|
57
108
|
*/
|
|
58
109
|
assessRepetition({ actionCalls, writeCalls }) {
|
|
59
110
|
const commands = [...(actionCalls || []), ...(writeCalls || [])];
|
|
60
|
-
if (commands.length === 0) {
|
|
61
|
-
this.#lastFingerprint = null;
|
|
62
|
-
this.#repetitionCount = 0;
|
|
63
|
-
return { continue: true };
|
|
64
|
-
}
|
|
111
|
+
if (commands.length === 0) return { continue: true };
|
|
65
112
|
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
const reason = `Same commands repeated ${this.#repetitionCount} turns`;
|
|
75
|
-
console.warn(`[RUMMY] Loop detected: ${reason}. Force-completing.`);
|
|
76
|
-
return { continue: false, reason };
|
|
77
|
-
}
|
|
78
|
-
console.warn(
|
|
79
|
-
`[RUMMY] Repeated commands (${this.#repetitionCount}/${MAX_REPETITIONS}): ${fingerprint.slice(0, 80)}`,
|
|
80
|
-
);
|
|
81
|
-
} else {
|
|
82
|
-
this.#repetitionCount = 1;
|
|
83
|
-
this.#lastFingerprint = fingerprint;
|
|
113
|
+
const fp = commands.map(cmdFingerprint).toSorted().join("|");
|
|
114
|
+
this.#turnHistory.push(fp);
|
|
115
|
+
|
|
116
|
+
const cycle = detectCycle(this.#turnHistory);
|
|
117
|
+
if (cycle.detected) {
|
|
118
|
+
const reason = `Cyclic tool pattern (period ${cycle.period}, ${cycle.cycles} repetitions)`;
|
|
119
|
+
console.warn(`[RUMMY] Loop detected: ${reason}. Force-completing.`);
|
|
120
|
+
return { continue: false, reason };
|
|
84
121
|
}
|
|
85
122
|
|
|
86
123
|
return { continue: true };
|
|
@@ -93,11 +130,12 @@ export default class ResponseHealer {
|
|
|
93
130
|
*
|
|
94
131
|
* Rules:
|
|
95
132
|
* <summarize/> present → done (terminate)
|
|
133
|
+
* <summarize/> + failed actions → overridden to <update> (continue)
|
|
96
134
|
* <update/> present → continue (model says it's working)
|
|
97
135
|
* neither present → warn, increment stall counter, continue
|
|
98
136
|
* stall counter hits MAX_STALLS → force-complete
|
|
99
137
|
*/
|
|
100
|
-
assessProgress({ summaryText, updateText, statusHealed }) {
|
|
138
|
+
assessProgress({ summaryText, updateText, statusHealed, flags }) {
|
|
101
139
|
if (summaryText) {
|
|
102
140
|
this.#stallCount = 0;
|
|
103
141
|
return { continue: false };
|
|
@@ -105,6 +143,21 @@ export default class ResponseHealer {
|
|
|
105
143
|
|
|
106
144
|
if (updateText && !statusHealed) {
|
|
107
145
|
this.#stallCount = 0;
|
|
146
|
+
// Track repeated update text — model stuck declaring readiness
|
|
147
|
+
// But if the model created new entries this turn, it's making
|
|
148
|
+
// progress even if the update text is the same.
|
|
149
|
+
const madeProgress = flags?.hasWrites || flags?.hasReads;
|
|
150
|
+
if (updateText === this.#lastUpdateText && !madeProgress) {
|
|
151
|
+
this.#updateRepeatCount++;
|
|
152
|
+
if (this.#updateRepeatCount >= MAX_UPDATE_REPEATS) {
|
|
153
|
+
const reason = `Same <update/> repeated ${this.#updateRepeatCount} turns: "${updateText.slice(0, 60)}"`;
|
|
154
|
+
console.warn(`[RUMMY] Stalled: ${reason}. Force-completing.`);
|
|
155
|
+
return { continue: false, reason };
|
|
156
|
+
}
|
|
157
|
+
} else {
|
|
158
|
+
this.#lastUpdateText = updateText;
|
|
159
|
+
this.#updateRepeatCount = 1;
|
|
160
|
+
}
|
|
108
161
|
return { continue: true };
|
|
109
162
|
}
|
|
110
163
|
|
|
@@ -128,7 +181,8 @@ export default class ResponseHealer {
|
|
|
128
181
|
*/
|
|
129
182
|
reset() {
|
|
130
183
|
this.#stallCount = 0;
|
|
131
|
-
this.#
|
|
132
|
-
this.#
|
|
184
|
+
this.#turnHistory = [];
|
|
185
|
+
this.#lastUpdateText = null;
|
|
186
|
+
this.#updateRepeatCount = 0;
|
|
133
187
|
}
|
|
134
188
|
}
|