@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.
Files changed (47) hide show
  1. package/.env.example +2 -1
  2. package/PLUGINS.md +1 -1
  3. package/SPEC.md +181 -38
  4. package/migrations/001_initial_schema.sql +1 -1
  5. package/package.json +7 -3
  6. package/service.js +5 -3
  7. package/src/agent/AgentLoop.js +182 -136
  8. package/src/agent/ContextAssembler.js +2 -0
  9. package/src/agent/KnownStore.js +28 -85
  10. package/src/agent/ResponseHealer.js +65 -31
  11. package/src/agent/TurnExecutor.js +326 -181
  12. package/src/agent/XmlParser.js +5 -2
  13. package/src/agent/known_store.sql +48 -0
  14. package/src/agent/tokens.js +1 -0
  15. package/src/agent/turns.sql +5 -0
  16. package/src/hooks/HookRegistry.js +7 -0
  17. package/src/hooks/Hooks.js +1 -4
  18. package/src/hooks/ToolRegistry.js +2 -8
  19. package/src/plugins/budget/README.md +2 -14
  20. package/src/plugins/budget/budget.js +15 -39
  21. package/src/plugins/cp/cp.js +1 -1
  22. package/src/plugins/cp/cpDoc.js +1 -1
  23. package/src/plugins/get/get.js +71 -1
  24. package/src/plugins/get/getDoc.js +14 -4
  25. package/src/plugins/hedberg/matcher.js +10 -29
  26. package/src/plugins/instructions/preamble.md +16 -6
  27. package/src/plugins/known/known.js +4 -10
  28. package/src/plugins/known/knownDoc.js +15 -14
  29. package/src/plugins/mv/mv.js +18 -1
  30. package/src/plugins/mv/mvDoc.js +15 -1
  31. package/src/plugins/{current → performed}/README.md +4 -3
  32. package/src/plugins/{current/current.js → performed/performed.js} +15 -20
  33. package/src/plugins/previous/README.md +2 -1
  34. package/src/plugins/previous/previous.js +31 -25
  35. package/src/plugins/progress/README.md +1 -2
  36. package/src/plugins/progress/progress.js +15 -29
  37. package/src/plugins/prompt/prompt.js +0 -7
  38. package/src/plugins/rm/rm.js +27 -15
  39. package/src/plugins/rm/rmDoc.js +3 -3
  40. package/src/plugins/set/set.js +55 -19
  41. package/src/plugins/set/setDoc.js +6 -2
  42. package/src/plugins/telemetry/telemetry.js +14 -9
  43. package/src/plugins/unknown/README.md +2 -1
  44. package/src/plugins/unknown/unknown.js +5 -4
  45. package/src/server/ClientConnection.js +59 -45
  46. package/src/sql/v_model_context.sql +3 -13
  47. 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 MAX_REPETITIONS = Number(process.env.RUMMY_MAX_REPETITIONS) || 3;
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
- #lastFingerprint = null;
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
- * Check for repeated tool commands across turns.
99
+ * Detect cyclic tool patterns across turns.
56
100
  * Returns { continue: boolean, reason?: string }
57
101
  *
58
- * Fingerprints the commands (name + path/query). If the same fingerprint
59
- * repeats for MAX_REPETITIONS consecutive turns, force-complete.
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 fingerprint = commands
70
- .map((c) => `${c.name}:${c.path || c.command || c.question || ""}`)
71
- .toSorted()
72
- .join("|");
73
-
74
- if (fingerprint === this.#lastFingerprint) {
75
- this.#repetitionCount++;
76
- if (this.#repetitionCount >= MAX_REPETITIONS) {
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.#lastFingerprint = null;
150
- this.#repetitionCount = 0;
184
+ this.#turnHistory = [];
151
185
  this.#lastUpdateText = null;
152
186
  this.#updateRepeatCount = 0;
153
187
  }