@possumtech/rummy 0.3.0 → 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 +2 -1
- package/PLUGINS.md +1 -1
- package/SPEC.md +181 -38
- package/migrations/001_initial_schema.sql +1 -1
- package/package.json +7 -3
- package/service.js +5 -3
- package/src/agent/AgentLoop.js +182 -136
- package/src/agent/ContextAssembler.js +2 -0
- package/src/agent/KnownStore.js +28 -85
- package/src/agent/ResponseHealer.js +65 -31
- package/src/agent/TurnExecutor.js +326 -181
- package/src/agent/XmlParser.js +5 -2
- package/src/agent/known_store.sql +48 -0
- 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 +1 -4
- package/src/hooks/ToolRegistry.js +2 -8
- package/src/plugins/budget/README.md +2 -14
- package/src/plugins/budget/budget.js +15 -39
- package/src/plugins/cp/cp.js +1 -1
- package/src/plugins/cp/cpDoc.js +1 -1
- package/src/plugins/get/get.js +71 -1
- package/src/plugins/get/getDoc.js +14 -4
- package/src/plugins/hedberg/matcher.js +10 -29
- package/src/plugins/instructions/preamble.md +16 -6
- package/src/plugins/known/known.js +4 -10
- package/src/plugins/known/knownDoc.js +15 -14
- package/src/plugins/mv/mv.js +18 -1
- package/src/plugins/mv/mvDoc.js +15 -1
- package/src/plugins/{current → performed}/README.md +4 -3
- package/src/plugins/{current/current.js → performed/performed.js} +15 -20
- 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 +15 -29
- package/src/plugins/prompt/prompt.js +0 -7
- package/src/plugins/rm/rm.js +27 -15
- package/src/plugins/rm/rmDoc.js +3 -3
- package/src/plugins/set/set.js +55 -19
- package/src/plugins/set/setDoc.js +6 -2
- package/src/plugins/telemetry/telemetry.js +14 -9
- package/src/plugins/unknown/README.md +2 -1
- package/src/plugins/unknown/unknown.js +5 -4
- package/src/server/ClientConnection.js +59 -45
- package/src/sql/v_model_context.sql +3 -13
- package/src/plugins/budget/BudgetGuard.js +0 -74
|
@@ -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
|
}
|