@possumtech/rummy 0.2.7 → 0.2.8

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 (59) hide show
  1. package/.env.example +1 -2
  2. package/PLUGINS.md +105 -82
  3. package/migrations/001_initial_schema.sql +53 -68
  4. package/package.json +4 -6
  5. package/service.js +1 -1
  6. package/src/agent/AgentLoop.js +91 -58
  7. package/src/agent/ContextAssembler.js +2 -2
  8. package/src/agent/KnownStore.js +30 -11
  9. package/src/agent/ProjectAgent.js +1 -3
  10. package/src/agent/TurnExecutor.js +119 -31
  11. package/src/agent/XmlParser.js +20 -0
  12. package/src/agent/known_checks.sql +5 -4
  13. package/src/agent/known_queries.sql +4 -3
  14. package/src/agent/known_store.sql +29 -15
  15. package/src/agent/loops.sql +63 -0
  16. package/src/agent/runs.sql +7 -7
  17. package/src/agent/schemes.sql +2 -2
  18. package/src/agent/turns.sql +3 -3
  19. package/src/hooks/PluginContext.js +1 -10
  20. package/src/hooks/RummyContext.js +16 -8
  21. package/src/plugins/ask_user/ask_user.js +3 -2
  22. package/src/plugins/cp/cp.js +7 -7
  23. package/src/plugins/current/current.js +3 -4
  24. package/src/plugins/engine/engine.sql +5 -3
  25. package/src/plugins/engine/turn_context.sql +9 -4
  26. package/src/plugins/env/docs.md +2 -0
  27. package/src/plugins/env/env.js +3 -2
  28. package/src/plugins/file/file.js +9 -19
  29. package/src/plugins/get/docs.md +7 -3
  30. package/src/plugins/get/get.js +22 -6
  31. package/src/plugins/hedberg/docs.md +0 -9
  32. package/src/plugins/hedberg/hedberg.js +2 -5
  33. package/src/plugins/hedberg/matcher.js +1 -1
  34. package/src/plugins/hedberg/patterns.js +6 -6
  35. package/src/plugins/helpers.js +2 -2
  36. package/src/plugins/index.js +28 -15
  37. package/src/plugins/instructions/instructions.js +1 -1
  38. package/src/plugins/known/known.js +9 -11
  39. package/src/plugins/mv/mv.js +7 -7
  40. package/src/plugins/previous/previous.js +6 -5
  41. package/src/plugins/progress/progress.js +6 -0
  42. package/src/plugins/prompt/prompt.js +9 -10
  43. package/src/plugins/rm/docs.md +3 -1
  44. package/src/plugins/rm/rm.js +24 -7
  45. package/src/plugins/rpc/rpc.js +33 -42
  46. package/src/plugins/set/docs.md +3 -1
  47. package/src/plugins/set/set.js +22 -16
  48. package/src/plugins/sh/sh.js +3 -2
  49. package/src/plugins/skills/skills.js +3 -4
  50. package/src/plugins/store/docs.md +2 -1
  51. package/src/plugins/store/store.js +14 -3
  52. package/src/plugins/summarize/summarize.js +1 -1
  53. package/src/plugins/telemetry/telemetry.js +17 -7
  54. package/src/plugins/unknown/unknown.js +3 -2
  55. package/src/plugins/update/update.js +1 -1
  56. package/src/server/ClientConnection.js +3 -5
  57. package/src/sql/v_model_context.sql +20 -23
  58. package/src/sql/v_run_log.sql +3 -3
  59. package/src/agent/prompt_queue.sql +0 -39
@@ -119,7 +119,7 @@ export default class AgentLoop {
119
119
  if (runInfo.blocked) {
120
120
  return {
121
121
  run: runInfo.alias,
122
- status: "proposed",
122
+ status: 202,
123
123
  remainingCount: runInfo.proposed.length,
124
124
  proposed: runInfo.proposed,
125
125
  };
@@ -127,8 +127,10 @@ export default class AgentLoop {
127
127
 
128
128
  const { runId: currentRunId, alias: currentAlias } = runInfo;
129
129
 
130
- await this.#db.enqueue_prompt.get({
130
+ const loopSeq = await this.#db.next_loop.get({ run_id: currentRunId });
131
+ await this.#db.enqueue_loop.get({
131
132
  run_id: currentRunId,
133
+ sequence: loopSeq.sequence,
132
134
  mode,
133
135
  model: requestedModel,
134
136
  prompt: prompt || "",
@@ -136,7 +138,7 @@ export default class AgentLoop {
136
138
  });
137
139
 
138
140
  if (this.#activeRuns.has(currentRunId)) {
139
- return { run: currentAlias, status: "queued" };
141
+ return { run: currentAlias, status: 100 };
140
142
  }
141
143
 
142
144
  return this.#drainQueue(
@@ -150,35 +152,37 @@ export default class AgentLoop {
150
152
 
151
153
  async #drainQueue(currentRunId, currentAlias, projectId, project, options) {
152
154
  while (true) {
153
- const queued = await this.#db.claim_next_prompt.get({
155
+ const loop = await this.#db.claim_next_loop.get({
154
156
  run_id: currentRunId,
155
157
  });
156
- if (!queued) break;
158
+ if (!loop) break;
157
159
 
158
- const promptConfig = queued.config ? JSON.parse(queued.config) : {};
160
+ const loopConfig = loop.config ? JSON.parse(loop.config) : {};
159
161
  const result = await this.#executeLoop({
160
- mode: queued.mode,
162
+ mode: loop.mode,
161
163
  project,
162
164
  projectId,
163
165
  currentRunId,
164
166
  currentAlias,
165
- requestedModel: queued.model,
166
- prompt: queued.prompt,
167
- noContext: promptConfig.noContext || false,
168
- options: { ...options, temperature: promptConfig.temperature },
169
- hook: queued.mode === "ask" ? this.#hooks.ask : this.#hooks.act,
167
+ currentLoopId: loop.id,
168
+ requestedModel: loop.model,
169
+ prompt: loop.prompt,
170
+ noContext: loopConfig.noContext || false,
171
+ options: { ...options, temperature: loopConfig.temperature },
172
+ hook: loop.mode === "ask" ? this.#hooks.ask : this.#hooks.act,
170
173
  });
171
174
 
172
- await this.#db.complete_prompt.run({
173
- id: queued.id,
175
+ await this.#db.complete_loop.run({
176
+ id: loop.id,
177
+ status: result.status === 202 ? 202 : result.status,
174
178
  result: JSON.stringify(result),
175
179
  });
176
180
 
177
- if (result.status === "proposed") return result;
181
+ if (result.status === 202) return result;
178
182
  }
179
183
 
180
184
  const runRow = await this.#db.get_run_by_alias.get({ alias: currentAlias });
181
- return { run: currentAlias, status: runRow?.status || "completed" };
185
+ return { run: currentAlias, status: runRow?.status ?? 200 };
182
186
  }
183
187
 
184
188
  async #executeLoop({
@@ -187,6 +191,7 @@ export default class AgentLoop {
187
191
  projectId,
188
192
  currentRunId,
189
193
  currentAlias,
194
+ currentLoopId,
190
195
  requestedModel,
191
196
  prompt,
192
197
  noContext,
@@ -194,10 +199,10 @@ export default class AgentLoop {
194
199
  hook,
195
200
  }) {
196
201
  const runRow = await this.#db.get_run_by_id.get({ id: currentRunId });
197
- if (runRow.status !== "running") {
202
+ if (runRow.status !== 102) {
198
203
  await this.#db.update_run_status.run({
199
204
  id: currentRunId,
200
- status: "running",
205
+ status: 102,
201
206
  });
202
207
  }
203
208
 
@@ -219,11 +224,11 @@ export default class AgentLoop {
219
224
  if (controller.signal.aborted) {
220
225
  await this.#db.update_run_status.run({
221
226
  id: currentRunId,
222
- status: "aborted",
227
+ status: 499,
223
228
  });
224
229
  const out = {
225
230
  run: currentAlias,
226
- status: "aborted",
231
+ status: 499,
227
232
  turn: loopIteration,
228
233
  };
229
234
  await hook.completed.emit({ projectId, ...out });
@@ -247,6 +252,7 @@ export default class AgentLoop {
247
252
  projectId,
248
253
  currentRunId,
249
254
  currentAlias,
255
+ currentLoopId,
250
256
  requestedModel,
251
257
  loopPrompt: turnPrompt,
252
258
  noContext,
@@ -265,14 +271,14 @@ export default class AgentLoop {
265
271
  const unresolved = await this.#knownStore.getUnresolved(currentRunId);
266
272
 
267
273
  const latestSummary = history
268
- .filter((e) => e.status === "summary")
274
+ .filter((e) => e.status === 200 && e.path?.startsWith("summarize://"))
269
275
  .at(-1);
270
276
 
271
277
  await this.#hooks.run.state.emit({
272
278
  projectId,
273
279
  run: currentAlias,
274
280
  turn: result.turn,
275
- status: unresolved.length > 0 ? "proposed" : "running",
281
+ status: unresolved.length > 0 ? 202 : 102,
276
282
  summary: latestSummary?.body || "",
277
283
  history,
278
284
  unknowns: unknowns.map((u) => ({ path: u.path, body: u.body })),
@@ -286,6 +292,12 @@ export default class AgentLoop {
286
292
  model: result.model,
287
293
  temperature: result.temperature,
288
294
  context_size: result.contextSize,
295
+ context_tokens: (
296
+ await this.#db.get_turn_budget.get({
297
+ run_id: currentRunId,
298
+ turn: result.turn,
299
+ })
300
+ ).total,
289
301
  prompt_tokens: runUsage.prompt_tokens,
290
302
  cached_tokens: runUsage.cached_tokens,
291
303
  completion_tokens: runUsage.completion_tokens,
@@ -301,11 +313,11 @@ export default class AgentLoop {
301
313
  if (unresolved.length > 0) {
302
314
  await this.#db.update_run_status.run({
303
315
  id: currentRunId,
304
- status: "proposed",
316
+ status: 202,
305
317
  });
306
318
  const out = {
307
319
  run: currentAlias,
308
- status: "proposed",
320
+ status: 202,
309
321
  turn: result.turn,
310
322
  proposed: unresolved,
311
323
  };
@@ -324,11 +336,11 @@ export default class AgentLoop {
324
336
  if (!repetition.continue) {
325
337
  await this.#db.update_run_status.run({
326
338
  id: currentRunId,
327
- status: "completed",
339
+ status: 200,
328
340
  });
329
341
  const out = {
330
342
  run: currentAlias,
331
- status: "completed",
343
+ status: 200,
332
344
  turn: result.turn,
333
345
  reason: repetition.reason,
334
346
  };
@@ -341,11 +353,11 @@ export default class AgentLoop {
341
353
 
342
354
  await this.#db.update_run_status.run({
343
355
  id: currentRunId,
344
- status: "completed",
356
+ status: 200,
345
357
  });
346
358
  const out = {
347
359
  run: currentAlias,
348
- status: "completed",
360
+ status: 200,
349
361
  turn: result.turn,
350
362
  };
351
363
  await hook.completed.emit({ projectId, ...out });
@@ -354,11 +366,11 @@ export default class AgentLoop {
354
366
 
355
367
  await this.#db.update_run_status.run({
356
368
  id: currentRunId,
357
- status: "completed",
369
+ status: 200,
358
370
  });
359
371
  const out = {
360
372
  run: currentAlias,
361
- status: "completed",
373
+ status: 200,
362
374
  turn: loopIteration,
363
375
  };
364
376
  await hook.completed.emit({ projectId, ...out });
@@ -367,15 +379,15 @@ export default class AgentLoop {
367
379
  if (controller.signal.aborted) {
368
380
  await this.#db.update_run_status.run({
369
381
  id: currentRunId,
370
- status: "aborted",
382
+ status: 499,
371
383
  });
372
- return { run: currentAlias, status: "aborted", turn: loopIteration };
384
+ return { run: currentAlias, status: 499, turn: loopIteration };
373
385
  }
374
386
  console.warn(`[RUMMY] Run failed: ${err.message}`);
375
387
  console.warn(`[RUMMY] Stack: ${err.stack}`);
376
388
  await this.#db.update_run_status.run({
377
389
  id: currentRunId,
378
- status: "failed",
390
+ status: 500,
379
391
  });
380
392
  try {
381
393
  await this.#knownStore.upsert(
@@ -383,12 +395,13 @@ export default class AgentLoop {
383
395
  loopIteration,
384
396
  `error://${loopIteration}`,
385
397
  `${err.message}\n${err.stack}`,
386
- "info",
398
+ 500,
399
+ { loopId: currentLoopId },
387
400
  );
388
401
  } catch {}
389
402
  const out = {
390
403
  run: currentAlias,
391
- status: "failed",
404
+ status: 500,
392
405
  turn: loopIteration,
393
406
  error: err.message,
394
407
  };
@@ -415,14 +428,14 @@ export default class AgentLoop {
415
428
  attrs,
416
429
  output,
417
430
  );
418
- const state = action === "error" ? "error" : "pass";
419
- await this.#knownStore.resolve(runId, path, state, resolvedBody);
431
+ const status = action === "error" ? 500 : 200;
432
+ await this.#knownStore.resolve(runId, path, status, resolvedBody);
420
433
 
421
434
  // Store answer in attributes for ask_user
422
435
  if (path.startsWith("ask_user://") && output) {
423
436
  const turn = (await this.#db.get_run_by_id.get({ id: runId }))
424
437
  .next_turn;
425
- await this.#knownStore.upsert(runId, turn, path, resolvedBody, state, {
438
+ await this.#knownStore.upsert(runId, turn, path, resolvedBody, status, {
426
439
  attributes: { ...attrs, answer: output },
427
440
  });
428
441
  }
@@ -441,12 +454,7 @@ export default class AgentLoop {
441
454
  }
442
455
  }
443
456
  } else if (action === "reject") {
444
- await this.#knownStore.resolve(
445
- runId,
446
- path,
447
- "rejected",
448
- output || "rejected",
449
- );
457
+ await this.#knownStore.resolve(runId, path, 403, output || "rejected");
450
458
  } else {
451
459
  throw new Error(msg("error.resolution_invalid", { action }));
452
460
  }
@@ -455,23 +463,46 @@ export default class AgentLoop {
455
463
  if (unresolved.length > 0) {
456
464
  return {
457
465
  run: runAlias,
458
- status: "proposed",
466
+ status: 202,
459
467
  remainingCount: unresolved.length,
460
468
  proposed: unresolved,
461
469
  };
462
470
  }
463
471
 
464
- if (await this.#knownStore.hasRejections(runId)) {
465
- await this.#db.update_run_status.run({ id: runId, status: "completed" });
466
- return { run: runAlias, status: "completed" };
472
+ // Scope completion checks to the current loop
473
+ const currentLoop = await this.#db.get_current_loop.get({ run_id: runId });
474
+ const loopId = currentLoop?.id ?? null;
475
+
476
+ if (await this.#knownStore.hasRejections(runId, loopId)) {
477
+ if (currentLoop)
478
+ await this.#db.complete_loop.run({
479
+ id: loopId,
480
+ status: 200,
481
+ result: null,
482
+ });
483
+ await this.#db.update_run_status.run({ id: runId, status: 200 });
484
+ return { run: runAlias, status: 200 };
467
485
  }
468
486
 
469
- const hasSummary = await this.#db.get_latest_summary.get({ run_id: runId });
487
+ const hasSummary = await this.#db.get_latest_summary.get({
488
+ run_id: runId,
489
+ loop_id: loopId,
490
+ });
470
491
  if (hasSummary?.body) {
471
- await this.#db.update_run_status.run({ id: runId, status: "completed" });
472
- return { run: runAlias, status: "completed" };
492
+ if (currentLoop)
493
+ await this.#db.complete_loop.run({
494
+ id: loopId,
495
+ status: 200,
496
+ result: null,
497
+ });
498
+ await this.#db.update_run_status.run({ id: runId, status: 200 });
499
+ return { run: runAlias, status: 200 };
473
500
  }
474
501
 
502
+ // No summary and no rejections in this loop — resume it
503
+ const projectId = runRow.project_id;
504
+ const project = await this.#db.get_project_by_id.get({ id: projectId });
505
+
475
506
  const latestPrompt = await this.#db.get_latest_prompt.get({
476
507
  run_id: runId,
477
508
  });
@@ -479,11 +510,11 @@ export default class AgentLoop {
479
510
  ? JSON.parse(latestPrompt.attributes).mode
480
511
  : "ask";
481
512
 
482
- const projectId = runRow.project_id;
483
- const project = await this.#db.get_project_by_id.get({ id: projectId });
484
-
485
- await this.#db.enqueue_prompt.get({
513
+ // Re-enqueue the current loop's prompt to continue it
514
+ const loopSeq = await this.#db.next_loop.get({ run_id: runId });
515
+ await this.#db.enqueue_loop.get({
486
516
  run_id: runId,
517
+ sequence: loopSeq.sequence,
487
518
  mode: resumeMode,
488
519
  model: runRow.model,
489
520
  prompt: "",
@@ -516,7 +547,7 @@ export default class AgentLoop {
516
547
  nextTurn,
517
548
  `prompt://${nextTurn}`,
518
549
  "",
519
- "info",
550
+ 200,
520
551
  { attributes: { mode: "ask" } },
521
552
  );
522
553
  await this.#knownStore.upsert(
@@ -524,15 +555,17 @@ export default class AgentLoop {
524
555
  nextTurn,
525
556
  `ask://${nextTurn}`,
526
557
  message,
527
- "info",
558
+ 200,
528
559
  );
529
560
 
530
561
  if (this.#activeRuns.has(runRow.id)) {
531
562
  return { run: runAlias, status: runRow.status, injected: "next_turn" };
532
563
  }
533
564
 
534
- await this.#db.enqueue_prompt.get({
565
+ const injectLoopSeq = await this.#db.next_loop.get({ run_id: runRow.id });
566
+ await this.#db.enqueue_loop.get({
535
567
  run_id: runRow.id,
568
+ sequence: injectLoopSeq.sequence,
536
569
  mode: "ask",
537
570
  model: runRow.model,
538
571
  prompt: message,
@@ -6,7 +6,7 @@
6
6
  export default class ContextAssembler {
7
7
  static async assembleFromTurnContext(
8
8
  rows,
9
- { type = "ask", systemPrompt = "", contextSize = 0 } = {},
9
+ { type = "ask", systemPrompt = "", contextSize = 0, demoted = [] } = {},
10
10
  hooks,
11
11
  ) {
12
12
  // Find loop boundary from active prompt
@@ -16,7 +16,7 @@ export default class ContextAssembler {
16
16
  );
17
17
  const loopStartTurn = promptEntry?.source_turn ?? 0;
18
18
 
19
- const ctx = { rows, loopStartTurn, type, contextSize };
19
+ const ctx = { rows, loopStartTurn, type, contextSize, demoted };
20
20
 
21
21
  const system = await hooks.assembly.system.filter(systemPrompt, ctx);
22
22
  const user = await hooks.assembly.user.filter("", ctx);
@@ -61,15 +61,23 @@ export default class KnownStore {
61
61
  turn,
62
62
  path,
63
63
  body,
64
- state,
65
- { attributes = null, hash = null, updatedAt = null } = {},
64
+ status,
65
+ {
66
+ fidelity = "full",
67
+ attributes = null,
68
+ hash = null,
69
+ updatedAt = null,
70
+ loopId = null,
71
+ } = {},
66
72
  ) {
67
73
  await this.#db.upsert_known_entry.run({
68
74
  run_id: runId,
75
+ loop_id: loopId,
69
76
  turn,
70
77
  path: KnownStore.normalizePath(path),
71
78
  body,
72
- state,
79
+ status,
80
+ fidelity,
73
81
  hash,
74
82
  attributes: attributes ? JSON.stringify(attributes) : null,
75
83
  updated_at: updatedAt,
@@ -84,17 +92,25 @@ export default class KnownStore {
84
92
  });
85
93
  }
86
94
 
87
- async setFileState(runId, pattern, state) {
88
- const result = await this.#db.set_file_state.run({
95
+ async setFileFidelity(runId, pattern, fidelity) {
96
+ const result = await this.#db.set_file_fidelity.run({
89
97
  run_id: runId,
90
98
  pattern,
91
- state,
99
+ fidelity,
92
100
  });
93
101
  if (result.changes === 0) {
94
- await this.upsert(runId, 0, pattern, "", state);
102
+ await this.upsert(runId, 0, pattern, "", 200, { fidelity });
95
103
  }
96
104
  }
97
105
 
106
+ async setFidelity(runId, path, fidelity) {
107
+ await this.#db.set_fidelity.run({
108
+ run_id: runId,
109
+ path: KnownStore.normalizePath(path),
110
+ fidelity,
111
+ });
112
+ }
113
+
98
114
  async demote(runId, path) {
99
115
  await this.#db.demote_path.run({
100
116
  run_id: runId,
@@ -164,11 +180,11 @@ export default class KnownStore {
164
180
  });
165
181
  }
166
182
 
167
- async resolve(runId, path, state, body) {
183
+ async resolve(runId, path, status, body) {
168
184
  await this.#db.resolve_known_entry.run({
169
185
  run_id: runId,
170
186
  path: KnownStore.normalizePath(path),
171
- state,
187
+ status,
172
188
  body,
173
189
  });
174
190
  }
@@ -185,8 +201,11 @@ export default class KnownStore {
185
201
  return this.#db.get_file_states_by_pattern.all({ run_id: runId, pattern });
186
202
  }
187
203
 
188
- async hasRejections(runId) {
189
- const row = await this.#db.has_rejections.get({ run_id: runId });
204
+ async hasRejections(runId, loopId) {
205
+ const row = await this.#db.has_rejections.get({
206
+ run_id: runId,
207
+ loop_id: loopId,
208
+ });
190
209
  return row.count > 0;
191
210
  }
192
211
 
@@ -40,7 +40,7 @@ export default class ProjectAgent {
40
40
  const projectRow = await this.#db.upsert_project.get({
41
41
  name: projectName,
42
42
  project_root: projectRoot,
43
- config_path: configPath || null,
43
+ config_path: configPath ?? null,
44
44
  });
45
45
  const projectId = projectRow.id;
46
46
 
@@ -57,8 +57,6 @@ export default class ProjectAgent {
57
57
  return this.#knownStore;
58
58
  }
59
59
 
60
- // --- Run operations ---
61
-
62
60
  async ask(projectId, model, prompt, run = null, options = {}) {
63
61
  return this.#agentLoop.run(
64
62
  "ask",