@possumtech/rummy 0.3.0 → 0.4.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/.env.example +13 -1
- package/PLUGINS.md +1 -1
- package/README.md +5 -1
- package/SPEC.md +211 -54
- package/migrations/001_initial_schema.sql +3 -4
- package/package.json +7 -3
- package/service.js +5 -3
- package/src/agent/AgentLoop.js +183 -238
- package/src/agent/ContextAssembler.js +2 -0
- package/src/agent/KnownStore.js +36 -85
- package/src/agent/ResponseHealer.js +65 -31
- package/src/agent/TurnExecutor.js +284 -382
- package/src/agent/XmlParser.js +28 -4
- package/src/agent/known_queries.sql +1 -1
- package/src/agent/known_store.sql +32 -34
- package/src/agent/runs.sql +2 -2
- package/src/agent/tokens.js +1 -0
- package/src/agent/turns.sql +5 -0
- package/src/hooks/HookRegistry.js +7 -0
- package/src/hooks/Hooks.js +2 -4
- package/src/hooks/ToolRegistry.js +8 -13
- package/src/plugins/ask_user/ask_userDoc.js +3 -8
- package/src/plugins/budget/README.md +26 -30
- package/src/plugins/budget/budget.js +69 -36
- package/src/plugins/budget/recovery.js +47 -0
- package/src/plugins/cp/cp.js +1 -1
- package/src/plugins/cp/cpDoc.js +5 -10
- package/src/plugins/env/envDoc.js +3 -8
- package/src/plugins/get/get.js +70 -2
- package/src/plugins/get/getDoc.js +19 -16
- package/src/plugins/hedberg/matcher.js +10 -29
- package/src/plugins/helpers.js +2 -2
- package/src/plugins/instructions/instructions.js +3 -2
- package/src/plugins/instructions/preamble.md +33 -12
- package/src/plugins/known/known.js +66 -17
- package/src/plugins/known/knownDoc.js +7 -10
- package/src/plugins/mv/mv.js +18 -1
- package/src/plugins/mv/mvDoc.js +9 -10
- package/src/plugins/{current → performed}/README.md +4 -3
- package/src/plugins/{current/current.js → performed/performed.js} +15 -20
- package/src/plugins/policy/policy.js +47 -0
- package/src/plugins/previous/README.md +2 -1
- package/src/plugins/previous/previous.js +31 -25
- package/src/plugins/progress/README.md +1 -2
- package/src/plugins/progress/progress.js +10 -60
- package/src/plugins/prompt/prompt.js +10 -8
- package/src/plugins/rm/rm.js +27 -15
- package/src/plugins/rm/rmDoc.js +6 -11
- package/src/plugins/rpc/rpc.js +3 -1
- package/src/plugins/set/set.js +125 -92
- package/src/plugins/set/setDoc.js +28 -37
- package/src/plugins/sh/shDoc.js +2 -7
- package/src/plugins/summarize/summarize.js +7 -0
- package/src/plugins/summarize/summarizeDoc.js +6 -11
- package/src/plugins/telemetry/telemetry.js +14 -9
- package/src/plugins/think/think.js +12 -0
- package/src/plugins/think/thinkDoc.js +18 -0
- package/src/plugins/unknown/README.md +2 -1
- package/src/plugins/unknown/unknown.js +26 -4
- package/src/plugins/unknown/unknownDoc.js +9 -14
- package/src/plugins/update/update.js +7 -0
- package/src/plugins/update/updateDoc.js +6 -11
- package/src/server/ClientConnection.js +69 -45
- package/src/sql/v_model_context.sql +7 -17
- package/src/plugins/budget/BudgetGuard.js +0 -74
package/src/agent/KnownStore.js
CHANGED
|
@@ -1,25 +1,17 @@
|
|
|
1
1
|
import slugify from "../sql/functions/slugify.js";
|
|
2
|
-
import { countTokens } from "./tokens.js";
|
|
3
2
|
|
|
4
3
|
export default class KnownStore {
|
|
5
4
|
#db;
|
|
6
5
|
#onChanged;
|
|
7
|
-
#budgetGuard = null;
|
|
8
6
|
#schemes = new Map();
|
|
7
|
+
#seq = 0;
|
|
8
|
+
#pendingResolutions = new Map();
|
|
9
9
|
|
|
10
10
|
constructor(db, { onChanged } = {}) {
|
|
11
11
|
this.#db = db;
|
|
12
12
|
this.#onChanged = onChanged || null;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
get budgetGuard() {
|
|
16
|
-
return this.#budgetGuard;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
set budgetGuard(guard) {
|
|
20
|
-
this.#budgetGuard = guard;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
15
|
async loadSchemes(db) {
|
|
24
16
|
const rows = await (db || this.#db).get_all_schemes.all();
|
|
25
17
|
this.#schemes.clear();
|
|
@@ -28,13 +20,6 @@ export default class KnownStore {
|
|
|
28
20
|
}
|
|
29
21
|
}
|
|
30
22
|
|
|
31
|
-
#isVisible(path, fidelity) {
|
|
32
|
-
if (fidelity === "archive") return false;
|
|
33
|
-
const scheme = KnownStore.scheme(path) ?? "file";
|
|
34
|
-
const meta = this.#schemes.get(scheme);
|
|
35
|
-
return meta ? meta.model_visible !== 0 : true;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
23
|
#emitChanged(runId, path, changeType) {
|
|
39
24
|
if (this.#onChanged) this.#onChanged({ runId, path, changeType });
|
|
40
25
|
}
|
|
@@ -46,15 +31,16 @@ export default class KnownStore {
|
|
|
46
31
|
|
|
47
32
|
static normalizePath(path) {
|
|
48
33
|
if (!path?.includes("://")) return path;
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
34
|
+
const sep = path.indexOf("://");
|
|
35
|
+
const scheme = path.slice(0, sep).toLowerCase();
|
|
36
|
+
const rest = path.slice(sep + 3);
|
|
37
|
+
try {
|
|
38
|
+
// Decode first (idempotent), then encode — but preserve slashes
|
|
39
|
+
const decoded = decodeURIComponent(rest);
|
|
40
|
+
return `${scheme}://${decoded.split("/").map(encodeURIComponent).join("/")}`;
|
|
41
|
+
} catch {
|
|
42
|
+
return `${scheme}://${rest.split("/").map(encodeURIComponent).join("/")}`;
|
|
43
|
+
}
|
|
58
44
|
}
|
|
59
45
|
|
|
60
46
|
async nextTurn(runId) {
|
|
@@ -71,15 +57,15 @@ export default class KnownStore {
|
|
|
71
57
|
path: candidate,
|
|
72
58
|
});
|
|
73
59
|
if (!existing) return candidate;
|
|
74
|
-
return `${candidate}_${
|
|
60
|
+
return `${candidate}_${++this.#seq}`;
|
|
75
61
|
}
|
|
76
62
|
|
|
77
63
|
async slugPath(runId, scheme, content, summary) {
|
|
78
|
-
const source = summary
|
|
64
|
+
const source = summary || content || "";
|
|
79
65
|
const base = slugify(source);
|
|
80
66
|
const prefix = `${scheme}://`;
|
|
81
67
|
|
|
82
|
-
if (!base) return `${prefix}${
|
|
68
|
+
if (!base) return `${prefix}${++this.#seq}`;
|
|
83
69
|
|
|
84
70
|
const candidate = `${prefix}${base}`;
|
|
85
71
|
const existing = await this.#db.get_entry_body.get({
|
|
@@ -88,7 +74,7 @@ export default class KnownStore {
|
|
|
88
74
|
});
|
|
89
75
|
if (!existing) return candidate;
|
|
90
76
|
|
|
91
|
-
return `${prefix}${base}_${
|
|
77
|
+
return `${prefix}${base}_${++this.#seq}`;
|
|
92
78
|
}
|
|
93
79
|
|
|
94
80
|
async upsert(
|
|
@@ -106,22 +92,6 @@ export default class KnownStore {
|
|
|
106
92
|
} = {},
|
|
107
93
|
) {
|
|
108
94
|
const normalized = KnownStore.normalizePath(path);
|
|
109
|
-
let delta = 0;
|
|
110
|
-
|
|
111
|
-
if (
|
|
112
|
-
this.#budgetGuard &&
|
|
113
|
-
status < 400 &&
|
|
114
|
-
this.#isVisible(normalized, fidelity)
|
|
115
|
-
) {
|
|
116
|
-
const existing = await this.#db.get_entry_body.get({
|
|
117
|
-
run_id: runId,
|
|
118
|
-
path: normalized,
|
|
119
|
-
});
|
|
120
|
-
delta =
|
|
121
|
-
countTokens(body) - (existing?.body ? countTokens(existing.body) : 0);
|
|
122
|
-
this.#budgetGuard.check(delta, normalized);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
95
|
await this.#db.upsert_known_entry.run({
|
|
126
96
|
run_id: runId,
|
|
127
97
|
loop_id: loopId,
|
|
@@ -135,8 +105,6 @@ export default class KnownStore {
|
|
|
135
105
|
updated_at: updatedAt,
|
|
136
106
|
});
|
|
137
107
|
this.#emitChanged(runId, normalized, "upsert");
|
|
138
|
-
|
|
139
|
-
if (delta > 0) this.#budgetGuard?.charge(delta);
|
|
140
108
|
}
|
|
141
109
|
|
|
142
110
|
async promote(runId, path, turn) {
|
|
@@ -202,21 +170,6 @@ export default class KnownStore {
|
|
|
202
170
|
}
|
|
203
171
|
|
|
204
172
|
async promoteByPattern(runId, path, body, turn) {
|
|
205
|
-
let cost = 0;
|
|
206
|
-
if (this.#budgetGuard) {
|
|
207
|
-
const entries = await this.#db.get_entries_by_pattern.all({
|
|
208
|
-
run_id: runId,
|
|
209
|
-
path,
|
|
210
|
-
body: KnownStore.#bodyPattern(body),
|
|
211
|
-
limit: null,
|
|
212
|
-
offset: null,
|
|
213
|
-
});
|
|
214
|
-
cost = entries
|
|
215
|
-
.filter((e) => e.fidelity === "archive" || e.fidelity === "index")
|
|
216
|
-
.reduce((sum, e) => sum + (e.tokens_full || 0), 0);
|
|
217
|
-
if (cost > 0) this.#budgetGuard.check(cost, path);
|
|
218
|
-
}
|
|
219
|
-
|
|
220
173
|
await this.#db.promote_by_pattern.run({
|
|
221
174
|
run_id: runId,
|
|
222
175
|
path,
|
|
@@ -224,8 +177,6 @@ export default class KnownStore {
|
|
|
224
177
|
turn,
|
|
225
178
|
});
|
|
226
179
|
this.#emitChanged(runId, path, "promote");
|
|
227
|
-
|
|
228
|
-
if (cost > 0) this.#budgetGuard?.charge(cost);
|
|
229
180
|
}
|
|
230
181
|
|
|
231
182
|
async demoteByPattern(runId, path, body) {
|
|
@@ -257,24 +208,6 @@ export default class KnownStore {
|
|
|
257
208
|
}
|
|
258
209
|
|
|
259
210
|
async updateBodyByPattern(runId, path, body, newBody) {
|
|
260
|
-
let delta = 0;
|
|
261
|
-
if (this.#budgetGuard) {
|
|
262
|
-
const entries = await this.#db.get_entries_by_pattern.all({
|
|
263
|
-
run_id: runId,
|
|
264
|
-
path,
|
|
265
|
-
body: KnownStore.#bodyPattern(body),
|
|
266
|
-
limit: null,
|
|
267
|
-
offset: null,
|
|
268
|
-
});
|
|
269
|
-
const visible = entries.filter((e) =>
|
|
270
|
-
this.#isVisible(e.path, e.fidelity),
|
|
271
|
-
);
|
|
272
|
-
const oldTotal = visible.reduce((sum, e) => sum + (e.tokens || 0), 0);
|
|
273
|
-
const newTokensPer = countTokens(newBody);
|
|
274
|
-
delta = newTokensPer * visible.length - oldTotal;
|
|
275
|
-
if (delta > 0) this.#budgetGuard.check(delta, path);
|
|
276
|
-
}
|
|
277
|
-
|
|
278
211
|
await this.#db.update_body_by_pattern.run({
|
|
279
212
|
run_id: runId,
|
|
280
213
|
path,
|
|
@@ -282,8 +215,6 @@ export default class KnownStore {
|
|
|
282
215
|
new_body: newBody,
|
|
283
216
|
});
|
|
284
217
|
this.#emitChanged(runId, path, "body");
|
|
285
|
-
|
|
286
|
-
if (delta > 0) this.#budgetGuard?.charge(delta);
|
|
287
218
|
}
|
|
288
219
|
|
|
289
220
|
async resolve(runId, path, status, body) {
|
|
@@ -295,8 +226,28 @@ export default class KnownStore {
|
|
|
295
226
|
body,
|
|
296
227
|
});
|
|
297
228
|
this.#emitChanged(runId, normalized, "resolve");
|
|
229
|
+
const key = `${runId}:${normalized}`;
|
|
230
|
+
const resolver = this.#pendingResolutions.get(key);
|
|
231
|
+
if (resolver) {
|
|
232
|
+
this.#pendingResolutions.delete(key);
|
|
233
|
+
resolver();
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
waitForResolution(runId, path) {
|
|
238
|
+
const normalized = KnownStore.normalizePath(path);
|
|
239
|
+
const key = `${runId}:${normalized}`;
|
|
240
|
+
return new Promise((resolve) => {
|
|
241
|
+
this.#pendingResolutions.set(key, resolve);
|
|
242
|
+
});
|
|
298
243
|
}
|
|
299
244
|
|
|
245
|
+
async restoreSummarizedPrompts(runId) {
|
|
246
|
+
await this.#db.restore_summarized_prompts.run({ run_id: runId });
|
|
247
|
+
this.#emitChanged(runId, "prompt://batch", "fidelity");
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
|
|
300
251
|
async getLog(runId) {
|
|
301
252
|
return this.#db.get_results.all({ run_id: runId });
|
|
302
253
|
}
|
|
@@ -1,11 +1,55 @@
|
|
|
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;
|
|
3
4
|
const MAX_UPDATE_REPEATS = Number(process.env.RUMMY_MAX_UPDATE_REPEATS) || 3;
|
|
4
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
|
+
}
|
|
49
|
+
|
|
5
50
|
export default class ResponseHealer {
|
|
6
51
|
#stallCount = 0;
|
|
7
|
-
#
|
|
8
|
-
#repetitionCount = 0;
|
|
52
|
+
#turnHistory = [];
|
|
9
53
|
#lastUpdateText = null;
|
|
10
54
|
#updateRepeatCount = 0;
|
|
11
55
|
|
|
@@ -52,38 +96,28 @@ export default class ResponseHealer {
|
|
|
52
96
|
}
|
|
53
97
|
|
|
54
98
|
/**
|
|
55
|
-
*
|
|
99
|
+
* Detect cyclic tool patterns across turns.
|
|
56
100
|
* Returns { continue: boolean, reason?: string }
|
|
57
101
|
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
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.
|
|
60
108
|
*/
|
|
61
109
|
assessRepetition({ actionCalls, writeCalls }) {
|
|
62
110
|
const commands = [...(actionCalls || []), ...(writeCalls || [])];
|
|
63
|
-
if (commands.length === 0) {
|
|
64
|
-
this.#lastFingerprint = null;
|
|
65
|
-
this.#repetitionCount = 0;
|
|
66
|
-
return { continue: true };
|
|
67
|
-
}
|
|
111
|
+
if (commands.length === 0) return { continue: true };
|
|
68
112
|
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
const reason = `Same commands repeated ${this.#repetitionCount} turns`;
|
|
78
|
-
console.warn(`[RUMMY] Loop detected: ${reason}. Force-completing.`);
|
|
79
|
-
return { continue: false, reason };
|
|
80
|
-
}
|
|
81
|
-
console.warn(
|
|
82
|
-
`[RUMMY] Repeated commands (${this.#repetitionCount}/${MAX_REPETITIONS}): ${fingerprint.slice(0, 80)}`,
|
|
83
|
-
);
|
|
84
|
-
} else {
|
|
85
|
-
this.#repetitionCount = 1;
|
|
86
|
-
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 };
|
|
87
121
|
}
|
|
88
122
|
|
|
89
123
|
return { continue: true };
|
|
@@ -96,6 +130,7 @@ export default class ResponseHealer {
|
|
|
96
130
|
*
|
|
97
131
|
* Rules:
|
|
98
132
|
* <summarize/> present → done (terminate)
|
|
133
|
+
* <summarize/> + failed actions → overridden to <update> (continue)
|
|
99
134
|
* <update/> present → continue (model says it's working)
|
|
100
135
|
* neither present → warn, increment stall counter, continue
|
|
101
136
|
* stall counter hits MAX_STALLS → force-complete
|
|
@@ -146,8 +181,7 @@ export default class ResponseHealer {
|
|
|
146
181
|
*/
|
|
147
182
|
reset() {
|
|
148
183
|
this.#stallCount = 0;
|
|
149
|
-
this.#
|
|
150
|
-
this.#repetitionCount = 0;
|
|
184
|
+
this.#turnHistory = [];
|
|
151
185
|
this.#lastUpdateText = null;
|
|
152
186
|
this.#updateRepeatCount = 0;
|
|
153
187
|
}
|