@possumtech/rummy 0.3.0 → 0.4.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 (65) hide show
  1. package/.env.example +13 -1
  2. package/PLUGINS.md +1 -1
  3. package/README.md +5 -1
  4. package/SPEC.md +211 -54
  5. package/migrations/001_initial_schema.sql +3 -4
  6. package/package.json +7 -3
  7. package/service.js +5 -3
  8. package/src/agent/AgentLoop.js +183 -238
  9. package/src/agent/ContextAssembler.js +2 -0
  10. package/src/agent/KnownStore.js +36 -85
  11. package/src/agent/ResponseHealer.js +65 -31
  12. package/src/agent/TurnExecutor.js +284 -382
  13. package/src/agent/XmlParser.js +28 -4
  14. package/src/agent/known_queries.sql +1 -1
  15. package/src/agent/known_store.sql +32 -34
  16. package/src/agent/runs.sql +2 -2
  17. package/src/agent/tokens.js +1 -0
  18. package/src/agent/turns.sql +5 -0
  19. package/src/hooks/HookRegistry.js +7 -0
  20. package/src/hooks/Hooks.js +2 -4
  21. package/src/hooks/ToolRegistry.js +8 -13
  22. package/src/plugins/ask_user/ask_userDoc.js +3 -8
  23. package/src/plugins/budget/README.md +26 -30
  24. package/src/plugins/budget/budget.js +69 -36
  25. package/src/plugins/budget/recovery.js +47 -0
  26. package/src/plugins/cp/cp.js +1 -1
  27. package/src/plugins/cp/cpDoc.js +5 -10
  28. package/src/plugins/env/envDoc.js +3 -8
  29. package/src/plugins/get/get.js +70 -2
  30. package/src/plugins/get/getDoc.js +19 -16
  31. package/src/plugins/hedberg/matcher.js +10 -29
  32. package/src/plugins/helpers.js +2 -2
  33. package/src/plugins/instructions/instructions.js +3 -2
  34. package/src/plugins/instructions/preamble.md +33 -12
  35. package/src/plugins/known/known.js +66 -17
  36. package/src/plugins/known/knownDoc.js +7 -10
  37. package/src/plugins/mv/mv.js +18 -1
  38. package/src/plugins/mv/mvDoc.js +9 -10
  39. package/src/plugins/{current → performed}/README.md +4 -3
  40. package/src/plugins/{current/current.js → performed/performed.js} +15 -20
  41. package/src/plugins/policy/policy.js +47 -0
  42. package/src/plugins/previous/README.md +2 -1
  43. package/src/plugins/previous/previous.js +31 -25
  44. package/src/plugins/progress/README.md +1 -2
  45. package/src/plugins/progress/progress.js +10 -60
  46. package/src/plugins/prompt/prompt.js +10 -8
  47. package/src/plugins/rm/rm.js +27 -15
  48. package/src/plugins/rm/rmDoc.js +6 -11
  49. package/src/plugins/rpc/rpc.js +3 -1
  50. package/src/plugins/set/set.js +125 -92
  51. package/src/plugins/set/setDoc.js +28 -37
  52. package/src/plugins/sh/shDoc.js +2 -7
  53. package/src/plugins/summarize/summarize.js +7 -0
  54. package/src/plugins/summarize/summarizeDoc.js +6 -11
  55. package/src/plugins/telemetry/telemetry.js +14 -9
  56. package/src/plugins/think/think.js +12 -0
  57. package/src/plugins/think/thinkDoc.js +18 -0
  58. package/src/plugins/unknown/README.md +2 -1
  59. package/src/plugins/unknown/unknown.js +26 -4
  60. package/src/plugins/unknown/unknownDoc.js +9 -14
  61. package/src/plugins/update/update.js +7 -0
  62. package/src/plugins/update/updateDoc.js +6 -11
  63. package/src/server/ClientConnection.js +69 -45
  64. package/src/sql/v_model_context.sql +7 -17
  65. package/src/plugins/budget/BudgetGuard.js +0 -74
@@ -7,6 +7,7 @@ export default class Update {
7
7
  this.#core = core;
8
8
  core.ensureTool();
9
9
  core.registerScheme({ category: "logging" });
10
+ core.on("handler", this.handler.bind(this));
10
11
  core.on("full", this.full.bind(this));
11
12
  core.on("summary", this.summary.bind(this));
12
13
  core.filter("instructions.toolDocs", async (docsMap) => {
@@ -15,6 +16,12 @@ export default class Update {
15
16
  });
16
17
  }
17
18
 
19
+ async handler(entry, rummy) {
20
+ const { entries: store, sequence: turn, runId, loopId } = rummy;
21
+ const statusPath = await store.slugPath(runId, "update", entry.body);
22
+ await store.upsert(runId, turn, statusPath, entry.body, 200, { loopId });
23
+ }
24
+
18
25
  full(entry) {
19
26
  return `# update\n${entry.body}`;
20
27
  }
@@ -2,31 +2,26 @@
2
2
  // Text goes to the model. Rationale stays in source.
3
3
  // Changing ANY line requires reading ALL rationales first.
4
4
  const LINES = [
5
- // --- Syntax
6
5
  ["## <update>[brief status]</update> - Signal continuation"],
7
-
8
- // --- Examples: research progress and multi-step work
9
6
  [
10
7
  "Example: <update>Reading config files</update>",
11
- "Progress checkpoint. Shows update as a status signal, not a log entry.",
8
+ "Progress checkpoint. Status signal, not a log entry.",
12
9
  ],
13
10
  [
14
11
  "Example: <update>Found 3 issues, fixing first</update>",
15
- "Multi-step progress. Shows update for ongoing work.",
12
+ "Multi-step progress. Ongoing work.",
16
13
  ],
17
-
18
- // --- Constraints: RFC-style MUST/MUST NOT
19
14
  [
20
- "* YOU MUST use <update> if still working — describes the current state",
21
- "Continuation signal. Triggers the next turn in the loop.",
15
+ "* YOU MUST use <update></update> if still working — describes the current state",
16
+ "Continuation signal. Triggers the next turn.",
22
17
  ],
23
18
  [
24
19
  "* YOU MUST NOT use <update> if done — use <summarize/> instead",
25
- "Mutual exclusion with summarize. Prevents infinite loops.",
20
+ "Mutual exclusion with summarize.",
26
21
  ],
27
22
  [
28
23
  "* YOU MUST keep <update> to <= 80 characters",
29
- "Length cap. Prevents models from writing essays in status updates.",
24
+ "Length cap.",
30
25
  ],
31
26
  ];
32
27
 
@@ -21,53 +21,76 @@ export default class ClientConnection {
21
21
  this.#projectAgent = new ProjectAgent(db, hooks);
22
22
 
23
23
  this.#ws.on("message", (data) => this.#handleMessage(data));
24
+ this.#ws.on("close", () => this.#teardown());
24
25
 
25
26
  this.#setupNotifications();
26
27
  }
27
28
 
28
- #setupNotifications() {
29
- this.#hooks.run.progress.on((payload) => {
30
- if (payload.projectId === this.#context.projectId) {
31
- this.#sendNotification("run/progress", {
32
- run: payload.run,
33
- turn: payload.turn,
34
- status: payload.status,
35
- });
36
- }
37
- });
29
+ #onProgress = (payload) => {
30
+ if (payload.projectId === this.#context.projectId) {
31
+ this.#sendNotification("run/progress", {
32
+ run: payload.run,
33
+ turn: payload.turn,
34
+ status: payload.status,
35
+ });
36
+ }
37
+ };
38
38
 
39
- this.#hooks.ui.render.on((payload) => {
40
- if (payload.projectId === this.#context.projectId) {
41
- this.#sendNotification("ui/render", {
42
- text: payload.text,
43
- append: payload.append,
44
- });
45
- }
46
- });
39
+ #onProposal = (payload) => {
40
+ if (payload.projectId === this.#context.projectId) {
41
+ this.#sendNotification("run/proposal", {
42
+ run: payload.run,
43
+ proposed: payload.proposed,
44
+ });
45
+ }
46
+ };
47
47
 
48
- this.#hooks.ui.notify.on((payload) => {
49
- if (payload.projectId === this.#context.projectId) {
50
- this.#sendNotification("ui/notify", {
51
- text: payload.text,
52
- level: payload.level,
53
- });
54
- }
55
- });
48
+ #onRender = (payload) => {
49
+ if (payload.projectId === this.#context.projectId) {
50
+ this.#sendNotification("ui/render", {
51
+ text: payload.text,
52
+ append: payload.append,
53
+ });
54
+ }
55
+ };
56
56
 
57
- this.#hooks.run.state.on((payload) => {
58
- if (payload.projectId === this.#context.projectId) {
59
- this.#sendNotification("run/state", {
60
- run: payload.run,
61
- turn: payload.turn,
62
- status: payload.status,
63
- summary: payload.summary,
64
- history: payload.history,
65
- unknowns: payload.unknowns,
66
- proposed: payload.proposed,
67
- telemetry: payload.telemetry,
68
- });
69
- }
70
- });
57
+ #onNotify = (payload) => {
58
+ if (payload.projectId === this.#context.projectId) {
59
+ this.#sendNotification("ui/notify", {
60
+ text: payload.text,
61
+ level: payload.level,
62
+ });
63
+ }
64
+ };
65
+
66
+ #onState = (payload) => {
67
+ if (payload.projectId === this.#context.projectId) {
68
+ this.#sendNotification("run/state", {
69
+ run: payload.run,
70
+ turn: payload.turn,
71
+ status: payload.status,
72
+ summary: payload.summary,
73
+ history: payload.history,
74
+ unknowns: payload.unknowns,
75
+ telemetry: payload.telemetry,
76
+ });
77
+ }
78
+ };
79
+
80
+ #setupNotifications() {
81
+ this.#hooks.run.progress.on(this.#onProgress);
82
+ this.#hooks.turn.proposal.on(this.#onProposal);
83
+ this.#hooks.ui.render.on(this.#onRender);
84
+ this.#hooks.ui.notify.on(this.#onNotify);
85
+ this.#hooks.run.state.on(this.#onState);
86
+ }
87
+
88
+ #teardown() {
89
+ this.#hooks.run.progress.off(this.#onProgress);
90
+ this.#hooks.turn.proposal.off(this.#onProposal);
91
+ this.#hooks.ui.render.off(this.#onRender);
92
+ this.#hooks.ui.notify.off(this.#onNotify);
93
+ this.#hooks.run.state.off(this.#onState);
71
94
  }
72
95
 
73
96
  #buildHandlerContext() {
@@ -135,10 +158,11 @@ export default class ClientConnection {
135
158
  );
136
159
  } else {
137
160
  const timeout = Number(process.env.RUMMY_RPC_TIMEOUT) || 10_000;
161
+ let timer;
138
162
  result = await Promise.race([
139
163
  registration.handler(params || {}, this.#buildHandlerContext()),
140
- new Promise((_, reject) =>
141
- setTimeout(
164
+ new Promise((_, reject) => {
165
+ timer = setTimeout(
142
166
  () =>
143
167
  reject(
144
168
  new Error(
@@ -149,9 +173,9 @@ export default class ClientConnection {
149
173
  ),
150
174
  ),
151
175
  timeout,
152
- ),
153
- ),
154
- ]);
176
+ );
177
+ }),
178
+ ]).finally(() => clearTimeout(timer));
155
179
  }
156
180
 
157
181
  const finalResult = await this.#hooks.rpc.response.result.filter(result, {
@@ -13,7 +13,8 @@ visible AS (
13
13
  , ke.turn
14
14
  , ke.updated_at
15
15
  , ke.attributes
16
- , ke.tokens AS tokens_full
16
+ , ke.tokens
17
+ , COALESCE(s.category, 'logging') AS category
17
18
  , CASE
18
19
  -- Archived entries not in context
19
20
  WHEN ke.fidelity = 'archive' THEN NULL
@@ -38,23 +39,13 @@ projected AS (
38
39
  , turn
39
40
  , updated_at
40
41
  , attributes
42
+ -- Category comes from schemes table — plugins declare it via registerScheme().
43
+ , category
44
+ , tokens
41
45
  , CASE
42
46
  WHEN visible_fidelity IN ('full', 'summary') THEN body
43
47
  ELSE ''
44
48
  END AS body
45
- -- Four roles: data, logging, unknown, prompt.
46
- -- These are structural — see PluginContext.CATEGORIES.
47
- -- 'tool' is internal (model_visible=0 in practice).
48
- -- Default is 'logging' — plugins opt into 'data' explicitly.
49
- , CASE
50
- WHEN scheme IS NULL THEN 'data'
51
- WHEN scheme IN ('http', 'https') THEN 'data'
52
- WHEN scheme IN ('known', 'skill') THEN 'data'
53
- WHEN scheme = 'unknown' THEN 'unknown'
54
- WHEN scheme = 'prompt' THEN 'prompt'
55
- WHEN scheme = 'tool' THEN 'tool'
56
- ELSE 'logging'
57
- END AS category
58
49
  FROM visible
59
50
  WHERE visible_fidelity IS NOT NULL
60
51
  )
@@ -81,9 +72,8 @@ SELECT
81
72
  END
82
73
  , CASE scheme WHEN 'skill' THEN 0 ELSE 1 END
83
74
  , CASE fidelity
84
- WHEN 'index' THEN 0
85
- WHEN 'summary' THEN 1
86
- ELSE 2
75
+ WHEN 'summary' THEN 0
76
+ ELSE 1
87
77
  END
88
78
  , turn
89
79
  , updated_at
@@ -1,74 +0,0 @@
1
- import { countTokens } from "../../agent/tokens.js";
2
-
3
- export class BudgetExceeded extends Error {
4
- constructor(path, requested, remaining) {
5
- super(
6
- `Budget exceeded: ${path} needs ${requested} tokens, ${remaining} remaining`,
7
- );
8
- this.name = "BudgetExceeded";
9
- this.status = 413;
10
- this.path = path;
11
- this.requested = requested;
12
- this.remaining = remaining;
13
- }
14
- }
15
-
16
- export default class BudgetGuard {
17
- #ceiling;
18
- #baseline;
19
- #spent;
20
- #tripped;
21
- #tripSource;
22
-
23
- constructor(ceiling, baseline) {
24
- this.#ceiling = ceiling ?? null;
25
- this.#baseline = baseline;
26
- this.#spent = 0;
27
- this.#tripped = false;
28
- this.#tripSource = null;
29
- }
30
-
31
- get isTripped() {
32
- return this.#tripped;
33
- }
34
-
35
- get tripSource() {
36
- return this.#tripSource;
37
- }
38
-
39
- get remaining() {
40
- if (this.#ceiling === null) return Infinity;
41
- return this.#ceiling - this.#baseline - this.#spent;
42
- }
43
-
44
- get spent() {
45
- return this.#spent;
46
- }
47
-
48
- check(tokens, path) {
49
- if (this.#ceiling === null) return;
50
- if (this.#tripped) throw new BudgetExceeded(path, tokens, 0);
51
- if (tokens <= 0) return;
52
- const remaining = this.remaining;
53
- if (tokens > remaining) throw new BudgetExceeded(path, tokens, remaining);
54
- }
55
-
56
- charge(tokens) {
57
- if (tokens > 0) this.#spent += tokens;
58
- }
59
-
60
- trip(source) {
61
- this.#tripped = true;
62
- this.#tripSource = source;
63
- }
64
-
65
- /**
66
- * Compute the token delta for an upsert. New entry = full cost.
67
- * Update = difference between new and old body.
68
- */
69
- static delta(newBody, existingBody) {
70
- const newTokens = countTokens(newBody);
71
- const oldTokens = existingBody ? countTokens(existingBody) : 0;
72
- return newTokens - oldTokens;
73
- }
74
- }