@possumtech/rummy 0.3.1 → 0.5.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.
Files changed (63) hide show
  1. package/.env.example +12 -0
  2. package/FIDELITY_CONTRACT.md +172 -0
  3. package/README.md +5 -1
  4. package/SPEC.md +31 -17
  5. package/migrations/001_initial_schema.sql +3 -4
  6. package/package.json +1 -1
  7. package/src/agent/AgentLoop.js +51 -153
  8. package/src/agent/ContextAssembler.js +2 -0
  9. package/src/agent/KnownStore.js +16 -9
  10. package/src/agent/ResponseHealer.js +54 -1
  11. package/src/agent/TurnExecutor.js +125 -323
  12. package/src/agent/XmlParser.js +172 -42
  13. package/src/agent/known_queries.sql +1 -1
  14. package/src/agent/known_store.sql +29 -72
  15. package/src/agent/runs.sql +2 -2
  16. package/src/hooks/Hooks.js +1 -0
  17. package/src/hooks/PluginContext.js +8 -2
  18. package/src/hooks/RummyContext.js +6 -3
  19. package/src/hooks/ToolRegistry.js +29 -32
  20. package/src/plugins/ask_user/ask_user.js +2 -2
  21. package/src/plugins/ask_user/ask_userDoc.js +7 -10
  22. package/src/plugins/budget/README.md +28 -18
  23. package/src/plugins/budget/budget.js +80 -3
  24. package/src/plugins/budget/recovery.js +47 -0
  25. package/src/plugins/cp/cp.js +5 -5
  26. package/src/plugins/cp/cpDoc.js +1 -14
  27. package/src/plugins/engine/engine.sql +1 -1
  28. package/src/plugins/env/env.js +4 -4
  29. package/src/plugins/env/envDoc.js +4 -9
  30. package/src/plugins/file/file.js +2 -7
  31. package/src/plugins/get/get.js +32 -13
  32. package/src/plugins/get/getDoc.js +26 -44
  33. package/src/plugins/helpers.js +4 -4
  34. package/src/plugins/instructions/instructions.js +9 -7
  35. package/src/plugins/instructions/preamble.md +45 -26
  36. package/src/plugins/known/known.js +71 -15
  37. package/src/plugins/known/knownDoc.js +4 -20
  38. package/src/plugins/mv/mv.js +6 -6
  39. package/src/plugins/mv/mvDoc.js +4 -30
  40. package/src/plugins/policy/policy.js +47 -0
  41. package/src/plugins/previous/previous.js +10 -14
  42. package/src/plugins/progress/progress.js +29 -48
  43. package/src/plugins/prompt/prompt.js +18 -6
  44. package/src/plugins/rm/rm.js +4 -4
  45. package/src/plugins/rm/rmDoc.js +5 -14
  46. package/src/plugins/rpc/rpc.js +4 -2
  47. package/src/plugins/set/set.js +86 -91
  48. package/src/plugins/set/setDoc.js +28 -41
  49. package/src/plugins/sh/sh.js +4 -4
  50. package/src/plugins/sh/shDoc.js +4 -9
  51. package/src/plugins/skill/skill.js +2 -1
  52. package/src/plugins/summarize/summarize.js +9 -2
  53. package/src/plugins/summarize/summarizeDoc.js +10 -16
  54. package/src/plugins/telemetry/telemetry.js +36 -11
  55. package/src/plugins/think/think.js +13 -0
  56. package/src/plugins/think/thinkDoc.js +16 -0
  57. package/src/plugins/unknown/unknown.js +37 -9
  58. package/src/plugins/unknown/unknownDoc.js +7 -16
  59. package/src/plugins/update/update.js +9 -2
  60. package/src/plugins/update/updateDoc.js +12 -14
  61. package/src/server/ClientConnection.js +11 -1
  62. package/src/sql/functions/slugify.js +13 -1
  63. package/src/sql/v_model_context.sql +6 -6
@@ -14,6 +14,7 @@ export default class ContextAssembler {
14
14
  toolSet = null,
15
15
  lastContextTokens = 0,
16
16
  turn = 1,
17
+ baselineTokens = 0,
17
18
  } = {},
18
19
  hooks,
19
20
  ) {
@@ -32,6 +33,7 @@ export default class ContextAssembler {
32
33
  demoted,
33
34
  toolSet,
34
35
  turn,
36
+ baselineTokens,
35
37
  };
36
38
 
37
39
  const system = await hooks.assembly.system.filter(systemPrompt, ctx);
@@ -5,6 +5,7 @@ export default class KnownStore {
5
5
  #onChanged;
6
6
  #schemes = new Map();
7
7
  #seq = 0;
8
+ #pendingResolutions = new Map();
8
9
 
9
10
  constructor(db, { onChanged } = {}) {
10
11
  this.#db = db;
@@ -83,7 +84,7 @@ export default class KnownStore {
83
84
  body,
84
85
  status,
85
86
  {
86
- fidelity = "full",
87
+ fidelity = "promoted",
87
88
  attributes = null,
88
89
  hash = null,
89
90
  updatedAt = null,
@@ -225,6 +226,20 @@ export default class KnownStore {
225
226
  body,
226
227
  });
227
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
+ });
228
243
  }
229
244
 
230
245
  async restoreSummarizedPrompts(runId) {
@@ -232,14 +247,6 @@ export default class KnownStore {
232
247
  this.#emitChanged(runId, "prompt://batch", "fidelity");
233
248
  }
234
249
 
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");
241
- }
242
-
243
250
  async getLog(runId) {
244
251
  return this.#db.get_results.all({ run_id: runId });
245
252
  }
@@ -2,6 +2,8 @@ const MAX_STALLS = Number(process.env.RUMMY_MAX_STALLS) || 3;
2
2
  const MIN_CYCLES = Number(process.env.RUMMY_MIN_CYCLES) || 3;
3
3
  const MAX_CYCLE_PERIOD = Number(process.env.RUMMY_MAX_CYCLE_PERIOD) || 4;
4
4
  const MAX_UPDATE_REPEATS = Number(process.env.RUMMY_MAX_UPDATE_REPEATS) || 3;
5
+ const MAX_PATH_STAGNATION =
6
+ Number(process.env.RUMMY_MAX_PATH_STAGNATION) || 5;
5
7
 
6
8
  /**
7
9
  * Build a stable fingerprint for a single recorded entry.
@@ -47,11 +49,28 @@ function detectCycle(history) {
47
49
  return { detected: false };
48
50
  }
49
51
 
52
+ /**
53
+ * Extract the target paths a command touches for stagnation detection.
54
+ * Same target logic as cmdFingerprint but returns the raw path for set
55
+ * comparison across turns.
56
+ */
57
+ function cmdPaths(entry) {
58
+ const attrs = entry.attributes ?? {};
59
+ const paths = [];
60
+ if (attrs.path) paths.push(attrs.path);
61
+ if (attrs.to) paths.push(attrs.to);
62
+ if (attrs.command) paths.push(attrs.command);
63
+ if (attrs.query) paths.push(attrs.query);
64
+ if (attrs.question) paths.push(attrs.question);
65
+ return paths;
66
+ }
67
+
50
68
  export default class ResponseHealer {
51
69
  #stallCount = 0;
52
70
  #turnHistory = [];
53
71
  #lastUpdateText = null;
54
72
  #updateRepeatCount = 0;
73
+ #pathRuns = new Map(); // path → consecutive turns touched
55
74
 
56
75
  /**
57
76
  * Heal a missing status tag. Called when the model emits
@@ -67,8 +86,15 @@ export default class ResponseHealer {
67
86
  static healStatus(content, commands) {
68
87
  const trimmed = content.trim();
69
88
 
89
+ // Detect malformed-glitch content — model attempted a tool invocation
90
+ // (native call, malformed XML, etc.) that the parser couldn't dispatch.
91
+ // This is NOT an answer; it's a glitch that deserves the 3-strikes
92
+ // stall path so the model can recover. Without this check, the model
93
+ // emits one malformed call and the run terminates after a single turn.
94
+ const looksGlitched = /<\|tool_call>|<tool_call\|>/.test(trimmed);
95
+
70
96
  // No commands + plain text = answered. Treat as summary.
71
- if (commands.length === 0 && trimmed) {
97
+ if (commands.length === 0 && trimmed && !looksGlitched) {
72
98
  console.warn("[RUMMY] Healed: plain text response treated as summary");
73
99
  return { summaryText: trimmed.slice(0, 500), updateText: null };
74
100
  }
@@ -120,6 +146,32 @@ export default class ResponseHealer {
120
146
  return { continue: false, reason };
121
147
  }
122
148
 
149
+ // Distinct-paths stagnation: the model might vary commands turn-to-turn
150
+ // (avoiding exact-cycle detection) but still churn on a single path.
151
+ // Track per-path consecutive touches; flag if any path is touched in
152
+ // MAX_PATH_STAGNATION consecutive turns. Catches semantic stagnation
153
+ // where the fingerprints differ in micro-detail but the work is stuck
154
+ // on one entry (e.g. endlessly re-setting/re-getting the same plan).
155
+ const touchedPaths = new Set();
156
+ for (const cmd of commands) {
157
+ for (const p of cmdPaths(cmd)) touchedPaths.add(p);
158
+ }
159
+ // Paths not touched this turn — run broken, remove from map.
160
+ for (const path of [...this.#pathRuns.keys()]) {
161
+ if (!touchedPaths.has(path)) this.#pathRuns.delete(path);
162
+ }
163
+ // Paths touched this turn — increment run.
164
+ for (const path of touchedPaths) {
165
+ this.#pathRuns.set(path, (this.#pathRuns.get(path) || 0) + 1);
166
+ }
167
+ for (const [path, run] of this.#pathRuns) {
168
+ if (run >= MAX_PATH_STAGNATION) {
169
+ const reason = `Path stagnation: ${path} touched ${run} consecutive turns`;
170
+ console.warn(`[RUMMY] ${reason}. Force-completing.`);
171
+ return { continue: false, reason };
172
+ }
173
+ }
174
+
123
175
  return { continue: true };
124
176
  }
125
177
 
@@ -184,5 +236,6 @@ export default class ResponseHealer {
184
236
  this.#turnHistory = [];
185
237
  this.#lastUpdateText = null;
186
238
  this.#updateRepeatCount = 0;
239
+ this.#pathRuns = new Map();
187
240
  }
188
241
  }