@possumtech/rummy 0.2.7 → 0.3.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 (119) hide show
  1. package/.env.example +12 -3
  2. package/EXCEPTIONS.md +46 -0
  3. package/PLUGINS.md +454 -197
  4. package/SPEC.md +284 -93
  5. package/migrations/001_initial_schema.sql +57 -70
  6. package/package.json +16 -10
  7. package/service.js +1 -1
  8. package/src/agent/AgentLoop.js +254 -70
  9. package/src/agent/ContextAssembler.js +18 -4
  10. package/src/agent/KnownStore.js +156 -23
  11. package/src/agent/ProjectAgent.js +5 -4
  12. package/src/agent/ResponseHealer.js +21 -1
  13. package/src/agent/TurnExecutor.js +393 -115
  14. package/src/agent/XmlParser.js +92 -39
  15. package/src/agent/known_checks.sql +5 -4
  16. package/src/agent/known_queries.sql +4 -3
  17. package/src/agent/known_store.sql +45 -15
  18. package/src/agent/loops.sql +63 -0
  19. package/src/agent/runs.sql +7 -7
  20. package/src/agent/schemes.sql +5 -2
  21. package/src/agent/tokens.js +6 -21
  22. package/src/agent/turns.sql +13 -4
  23. package/src/hooks/Hooks.js +18 -0
  24. package/src/hooks/PluginContext.js +14 -10
  25. package/src/hooks/RummyContext.js +30 -10
  26. package/src/hooks/ToolRegistry.js +83 -19
  27. package/src/llm/LlmProvider.js +27 -8
  28. package/src/llm/OpenAiClient.js +20 -0
  29. package/src/llm/OpenRouterClient.js +24 -2
  30. package/src/llm/XaiClient.js +47 -2
  31. package/src/plugins/ask_user/README.md +4 -4
  32. package/src/plugins/ask_user/ask_user.js +8 -7
  33. package/src/plugins/ask_user/ask_userDoc.js +29 -0
  34. package/src/plugins/budget/BudgetGuard.js +74 -0
  35. package/src/plugins/budget/README.md +43 -0
  36. package/src/plugins/budget/budget.js +79 -0
  37. package/src/plugins/cp/README.md +5 -4
  38. package/src/plugins/cp/cp.js +16 -12
  39. package/src/plugins/cp/cpDoc.js +29 -0
  40. package/src/plugins/current/README.md +4 -4
  41. package/src/plugins/current/current.js +12 -10
  42. package/src/plugins/engine/engine.sql +5 -10
  43. package/src/plugins/engine/turn_context.sql +13 -13
  44. package/src/plugins/env/README.md +3 -4
  45. package/src/plugins/env/env.js +8 -7
  46. package/src/plugins/env/envDoc.js +29 -0
  47. package/src/plugins/file/README.md +9 -12
  48. package/src/plugins/file/file.js +34 -45
  49. package/src/plugins/get/README.md +2 -2
  50. package/src/plugins/get/get.js +28 -11
  51. package/src/plugins/get/getDoc.js +41 -0
  52. package/src/plugins/hedberg/docs.md +0 -9
  53. package/src/plugins/hedberg/hedberg.js +4 -6
  54. package/src/plugins/hedberg/matcher.js +1 -1
  55. package/src/plugins/hedberg/normalize.js +28 -0
  56. package/src/plugins/hedberg/patterns.js +31 -33
  57. package/src/plugins/hedberg/sed.js +17 -10
  58. package/src/plugins/helpers.js +2 -2
  59. package/src/plugins/index.js +93 -28
  60. package/src/plugins/instructions/README.md +6 -2
  61. package/src/plugins/instructions/instructions.js +21 -5
  62. package/src/plugins/instructions/preamble.md +9 -5
  63. package/src/plugins/known/README.md +10 -7
  64. package/src/plugins/known/known.js +33 -23
  65. package/src/plugins/known/knownDoc.js +33 -0
  66. package/src/plugins/mv/README.md +5 -4
  67. package/src/plugins/mv/mv.js +16 -12
  68. package/src/plugins/mv/mvDoc.js +31 -0
  69. package/src/plugins/persona/persona.js +78 -0
  70. package/src/plugins/previous/README.md +2 -2
  71. package/src/plugins/previous/previous.js +12 -8
  72. package/src/plugins/progress/progress.js +44 -12
  73. package/src/plugins/prompt/README.md +5 -5
  74. package/src/plugins/prompt/prompt.js +23 -19
  75. package/src/plugins/rm/README.md +4 -4
  76. package/src/plugins/rm/rm.js +29 -12
  77. package/src/plugins/rm/rmDoc.js +30 -0
  78. package/src/plugins/rpc/README.md +15 -28
  79. package/src/plugins/rpc/rpc.js +63 -107
  80. package/src/plugins/set/README.md +13 -12
  81. package/src/plugins/set/set.js +82 -21
  82. package/src/plugins/set/setDoc.js +45 -0
  83. package/src/plugins/sh/README.md +4 -4
  84. package/src/plugins/sh/sh.js +8 -7
  85. package/src/plugins/sh/shDoc.js +29 -0
  86. package/src/plugins/{skills/skills.js → skill/skill.js} +12 -54
  87. package/src/plugins/summarize/README.md +6 -5
  88. package/src/plugins/summarize/summarize.js +7 -6
  89. package/src/plugins/summarize/summarizeDoc.js +33 -0
  90. package/src/plugins/telemetry/telemetry.js +20 -8
  91. package/src/plugins/think/README.md +20 -0
  92. package/src/plugins/think/think.js +5 -0
  93. package/src/plugins/unknown/README.md +5 -5
  94. package/src/plugins/unknown/unknown.js +11 -8
  95. package/src/plugins/unknown/unknownDoc.js +31 -0
  96. package/src/plugins/update/README.md +3 -8
  97. package/src/plugins/update/update.js +7 -6
  98. package/src/plugins/update/updateDoc.js +33 -0
  99. package/src/server/ClientConnection.js +3 -5
  100. package/src/server/RpcRegistry.js +52 -4
  101. package/src/sql/v_model_context.sql +31 -39
  102. package/src/sql/v_run_log.sql +3 -3
  103. package/src/agent/prompt_queue.sql +0 -39
  104. package/src/plugins/ask_user/docs.md +0 -2
  105. package/src/plugins/cp/docs.md +0 -2
  106. package/src/plugins/env/docs.md +0 -2
  107. package/src/plugins/get/docs.md +0 -6
  108. package/src/plugins/known/docs.md +0 -3
  109. package/src/plugins/mv/docs.md +0 -2
  110. package/src/plugins/rm/docs.md +0 -4
  111. package/src/plugins/set/docs.md +0 -4
  112. package/src/plugins/sh/docs.md +0 -2
  113. package/src/plugins/skills/README.md +0 -25
  114. package/src/plugins/store/README.md +0 -20
  115. package/src/plugins/store/docs.md +0 -5
  116. package/src/plugins/store/store.js +0 -52
  117. package/src/plugins/summarize/docs.md +0 -4
  118. package/src/plugins/unknown/docs.md +0 -5
  119. package/src/plugins/update/docs.md +0 -4
@@ -1,10 +1,42 @@
1
1
  import slugify from "../sql/functions/slugify.js";
2
+ import { countTokens } from "./tokens.js";
2
3
 
3
4
  export default class KnownStore {
4
5
  #db;
6
+ #onChanged;
7
+ #budgetGuard = null;
8
+ #schemes = new Map();
5
9
 
6
- constructor(db) {
10
+ constructor(db, { onChanged } = {}) {
7
11
  this.#db = db;
12
+ this.#onChanged = onChanged || null;
13
+ }
14
+
15
+ get budgetGuard() {
16
+ return this.#budgetGuard;
17
+ }
18
+
19
+ set budgetGuard(guard) {
20
+ this.#budgetGuard = guard;
21
+ }
22
+
23
+ async loadSchemes(db) {
24
+ const rows = await (db || this.#db).get_all_schemes.all();
25
+ this.#schemes.clear();
26
+ for (const row of rows) {
27
+ this.#schemes.set(row.name, row);
28
+ }
29
+ }
30
+
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
+ #emitChanged(runId, path, changeType) {
39
+ if (this.#onChanged) this.#onChanged({ runId, path, changeType });
8
40
  }
9
41
 
10
42
  static scheme(path) {
@@ -30,18 +62,21 @@ export default class KnownStore {
30
62
  return row.turn;
31
63
  }
32
64
 
33
- async dedup(runId, scheme, target) {
34
- const candidate = `${scheme}://${target}`;
65
+ async dedup(runId, scheme, target, turn) {
66
+ const encodedTarget = encodeURIComponent(target);
67
+ const turnPrefix = turn ? `turn_${turn}/` : "";
68
+ const candidate = `${scheme}://${turnPrefix}${encodedTarget}`;
35
69
  const existing = await this.#db.get_entry_body.get({
36
70
  run_id: runId,
37
- path: KnownStore.normalizePath(candidate),
71
+ path: candidate,
38
72
  });
39
73
  if (!existing) return candidate;
40
74
  return `${candidate}_${Date.now()}`;
41
75
  }
42
76
 
43
- async slugPath(runId, scheme, content) {
44
- const base = slugify(content || "");
77
+ async slugPath(runId, scheme, content, summary) {
78
+ const source = summary ? summary.replace(/,\s*/g, "/") : content || "";
79
+ const base = slugify(source);
45
80
  const prefix = `${scheme}://`;
46
81
 
47
82
  if (!base) return `${prefix}${Date.now()}`;
@@ -61,52 +96,97 @@ export default class KnownStore {
61
96
  turn,
62
97
  path,
63
98
  body,
64
- state,
65
- { attributes = null, hash = null, updatedAt = null } = {},
99
+ status,
100
+ {
101
+ fidelity = "full",
102
+ attributes = null,
103
+ hash = null,
104
+ updatedAt = null,
105
+ loopId = null,
106
+ } = {},
66
107
  ) {
108
+ 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
+
67
125
  await this.#db.upsert_known_entry.run({
68
126
  run_id: runId,
127
+ loop_id: loopId,
69
128
  turn,
70
- path: KnownStore.normalizePath(path),
129
+ path: normalized,
71
130
  body,
72
- state,
131
+ status,
132
+ fidelity,
73
133
  hash,
74
134
  attributes: attributes ? JSON.stringify(attributes) : null,
75
135
  updated_at: updatedAt,
76
136
  });
137
+ this.#emitChanged(runId, normalized, "upsert");
138
+
139
+ if (delta > 0) this.#budgetGuard?.charge(delta);
77
140
  }
78
141
 
79
142
  async promote(runId, path, turn) {
143
+ const normalized = KnownStore.normalizePath(path);
80
144
  await this.#db.promote_path.run({
81
145
  run_id: runId,
82
- path: KnownStore.normalizePath(path),
146
+ path: normalized,
83
147
  turn,
84
148
  });
149
+ this.#emitChanged(runId, normalized, "promote");
85
150
  }
86
151
 
87
- async setFileState(runId, pattern, state) {
88
- const result = await this.#db.set_file_state.run({
152
+ async setFileFidelity(runId, pattern, fidelity) {
153
+ const result = await this.#db.set_file_fidelity.run({
89
154
  run_id: runId,
90
155
  pattern,
91
- state,
156
+ fidelity,
92
157
  });
93
158
  if (result.changes === 0) {
94
- await this.upsert(runId, 0, pattern, "", state);
159
+ await this.upsert(runId, 0, pattern, "", 200, { fidelity });
95
160
  }
161
+ this.#emitChanged(runId, pattern, "fidelity");
162
+ }
163
+
164
+ async setFidelity(runId, path, fidelity) {
165
+ const normalized = KnownStore.normalizePath(path);
166
+ await this.#db.set_fidelity.run({
167
+ run_id: runId,
168
+ path: normalized,
169
+ fidelity,
170
+ });
171
+ this.#emitChanged(runId, normalized, "fidelity");
96
172
  }
97
173
 
98
174
  async demote(runId, path) {
175
+ const normalized = KnownStore.normalizePath(path);
99
176
  await this.#db.demote_path.run({
100
177
  run_id: runId,
101
- path: KnownStore.normalizePath(path),
178
+ path: normalized,
102
179
  });
180
+ this.#emitChanged(runId, normalized, "demote");
103
181
  }
104
182
 
105
183
  async remove(runId, path) {
184
+ const normalized = KnownStore.normalizePath(path);
106
185
  await this.#db.delete_known_entry.run({
107
186
  run_id: runId,
108
- path: KnownStore.normalizePath(path),
187
+ path: normalized,
109
188
  });
189
+ this.#emitChanged(runId, normalized, "remove");
110
190
  }
111
191
 
112
192
  async removeFilesByPattern(runId, pattern) {
@@ -114,6 +194,7 @@ export default class KnownStore {
114
194
  run_id: runId,
115
195
  pattern,
116
196
  });
197
+ this.#emitChanged(runId, pattern, "remove");
117
198
  }
118
199
 
119
200
  static #bodyPattern(body) {
@@ -121,12 +202,30 @@ export default class KnownStore {
121
202
  }
122
203
 
123
204
  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
+
124
220
  await this.#db.promote_by_pattern.run({
125
221
  run_id: runId,
126
222
  path,
127
223
  body: KnownStore.#bodyPattern(body),
128
224
  turn,
129
225
  });
226
+ this.#emitChanged(runId, path, "promote");
227
+
228
+ if (cost > 0) this.#budgetGuard?.charge(cost);
130
229
  }
131
230
 
132
231
  async demoteByPattern(runId, path, body) {
@@ -135,6 +234,7 @@ export default class KnownStore {
135
234
  path,
136
235
  body: KnownStore.#bodyPattern(body),
137
236
  });
237
+ this.#emitChanged(runId, path, "demote");
138
238
  }
139
239
 
140
240
  async getEntriesByPattern(runId, path, body, { limit, offset } = {}) {
@@ -153,30 +253,58 @@ export default class KnownStore {
153
253
  path,
154
254
  body: KnownStore.#bodyPattern(body),
155
255
  });
256
+ this.#emitChanged(runId, path, "remove");
156
257
  }
157
258
 
158
259
  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
+
159
278
  await this.#db.update_body_by_pattern.run({
160
279
  run_id: runId,
161
280
  path,
162
281
  body: KnownStore.#bodyPattern(body),
163
282
  new_body: newBody,
164
283
  });
284
+ this.#emitChanged(runId, path, "body");
285
+
286
+ if (delta > 0) this.#budgetGuard?.charge(delta);
165
287
  }
166
288
 
167
- async resolve(runId, path, state, body) {
289
+ async resolve(runId, path, status, body) {
290
+ const normalized = KnownStore.normalizePath(path);
168
291
  await this.#db.resolve_known_entry.run({
169
292
  run_id: runId,
170
- path: KnownStore.normalizePath(path),
171
- state,
293
+ path: normalized,
294
+ status,
172
295
  body,
173
296
  });
297
+ this.#emitChanged(runId, normalized, "resolve");
174
298
  }
175
299
 
176
300
  async getLog(runId) {
177
301
  return this.#db.get_results.all({ run_id: runId });
178
302
  }
179
303
 
304
+ async getEntries(runId) {
305
+ return this.#db.get_known_entries.all({ run_id: runId });
306
+ }
307
+
180
308
  async getFileEntries(runId) {
181
309
  return this.#db.get_file_entries.all({ run_id: runId });
182
310
  }
@@ -185,8 +313,11 @@ export default class KnownStore {
185
313
  return this.#db.get_file_states_by_pattern.all({ run_id: runId, pattern });
186
314
  }
187
315
 
188
- async hasRejections(runId) {
189
- const row = await this.#db.has_rejections.get({ run_id: runId });
316
+ async hasRejections(runId, loopId) {
317
+ const row = await this.#db.has_rejections.get({
318
+ run_id: runId,
319
+ loop_id: loopId,
320
+ });
190
321
  return row.count > 0;
191
322
  }
192
323
 
@@ -218,11 +349,13 @@ export default class KnownStore {
218
349
  }
219
350
 
220
351
  async setAttributes(runId, path, attrs) {
352
+ const normalized = KnownStore.normalizePath(path);
221
353
  await this.#db.update_entry_attributes.run({
222
354
  run_id: runId,
223
- path: KnownStore.normalizePath(path),
355
+ path: normalized,
224
356
  attributes: JSON.stringify(attrs),
225
357
  });
358
+ this.#emitChanged(runId, normalized, "attributes");
226
359
  }
227
360
 
228
361
  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,
@@ -40,7 +43,7 @@ export default class ProjectAgent {
40
43
  const projectRow = await this.#db.upsert_project.get({
41
44
  name: projectName,
42
45
  project_root: projectRoot,
43
- config_path: configPath || null,
46
+ config_path: configPath ?? null,
44
47
  });
45
48
  const projectId = projectRow.id;
46
49
 
@@ -57,8 +60,6 @@ export default class ProjectAgent {
57
60
  return this.#knownStore;
58
61
  }
59
62
 
60
- // --- Run operations ---
61
-
62
63
  async ask(projectId, model, prompt, run = null, options = {}) {
63
64
  return this.#agentLoop.run(
64
65
  "ask",
@@ -1,10 +1,13 @@
1
1
  const MAX_STALLS = Number(process.env.RUMMY_MAX_STALLS) || 3;
2
2
  const MAX_REPETITIONS = Number(process.env.RUMMY_MAX_REPETITIONS) || 3;
3
+ const MAX_UPDATE_REPEATS = Number(process.env.RUMMY_MAX_UPDATE_REPEATS) || 3;
3
4
 
4
5
  export default class ResponseHealer {
5
6
  #stallCount = 0;
6
7
  #lastFingerprint = null;
7
8
  #repetitionCount = 0;
9
+ #lastUpdateText = null;
10
+ #updateRepeatCount = 0;
8
11
 
9
12
  /**
10
13
  * Heal a missing status tag. Called when the model emits
@@ -97,7 +100,7 @@ export default class ResponseHealer {
97
100
  * neither present → warn, increment stall counter, continue
98
101
  * stall counter hits MAX_STALLS → force-complete
99
102
  */
100
- assessProgress({ summaryText, updateText, statusHealed }) {
103
+ assessProgress({ summaryText, updateText, statusHealed, flags }) {
101
104
  if (summaryText) {
102
105
  this.#stallCount = 0;
103
106
  return { continue: false };
@@ -105,6 +108,21 @@ export default class ResponseHealer {
105
108
 
106
109
  if (updateText && !statusHealed) {
107
110
  this.#stallCount = 0;
111
+ // Track repeated update text — model stuck declaring readiness
112
+ // But if the model created new entries this turn, it's making
113
+ // progress even if the update text is the same.
114
+ const madeProgress = flags?.hasWrites || flags?.hasReads;
115
+ if (updateText === this.#lastUpdateText && !madeProgress) {
116
+ this.#updateRepeatCount++;
117
+ if (this.#updateRepeatCount >= MAX_UPDATE_REPEATS) {
118
+ const reason = `Same <update/> repeated ${this.#updateRepeatCount} turns: "${updateText.slice(0, 60)}"`;
119
+ console.warn(`[RUMMY] Stalled: ${reason}. Force-completing.`);
120
+ return { continue: false, reason };
121
+ }
122
+ } else {
123
+ this.#lastUpdateText = updateText;
124
+ this.#updateRepeatCount = 1;
125
+ }
108
126
  return { continue: true };
109
127
  }
110
128
 
@@ -130,5 +148,7 @@ export default class ResponseHealer {
130
148
  this.#stallCount = 0;
131
149
  this.#lastFingerprint = null;
132
150
  this.#repetitionCount = 0;
151
+ this.#lastUpdateText = null;
152
+ this.#updateRepeatCount = 0;
133
153
  }
134
154
  }