@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
@@ -21,53 +21,66 @@ 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
+ #onRender = (payload) => {
40
+ if (payload.projectId === this.#context.projectId) {
41
+ this.#sendNotification("ui/render", {
42
+ text: payload.text,
43
+ append: payload.append,
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
+ #onNotify = (payload) => {
49
+ if (payload.projectId === this.#context.projectId) {
50
+ this.#sendNotification("ui/notify", {
51
+ text: payload.text,
52
+ level: payload.level,
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
+ #onState = (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
+ };
71
+
72
+ #setupNotifications() {
73
+ this.#hooks.run.progress.on(this.#onProgress);
74
+ this.#hooks.ui.render.on(this.#onRender);
75
+ this.#hooks.ui.notify.on(this.#onNotify);
76
+ this.#hooks.run.state.on(this.#onState);
77
+ }
78
+
79
+ #teardown() {
80
+ this.#hooks.run.progress.off(this.#onProgress);
81
+ this.#hooks.ui.render.off(this.#onRender);
82
+ this.#hooks.ui.notify.off(this.#onNotify);
83
+ this.#hooks.run.state.off(this.#onState);
71
84
  }
72
85
 
73
86
  #buildHandlerContext() {
@@ -135,10 +148,11 @@ export default class ClientConnection {
135
148
  );
136
149
  } else {
137
150
  const timeout = Number(process.env.RUMMY_RPC_TIMEOUT) || 10_000;
151
+ let timer;
138
152
  result = await Promise.race([
139
153
  registration.handler(params || {}, this.#buildHandlerContext()),
140
- new Promise((_, reject) =>
141
- setTimeout(
154
+ new Promise((_, reject) => {
155
+ timer = setTimeout(
142
156
  () =>
143
157
  reject(
144
158
  new Error(
@@ -149,9 +163,9 @@ export default class ClientConnection {
149
163
  ),
150
164
  ),
151
165
  timeout,
152
- ),
153
- ),
154
- ]);
166
+ );
167
+ }),
168
+ ]).finally(() => clearTimeout(timer));
155
169
  }
156
170
 
157
171
  const finalResult = await this.#hooks.rpc.response.result.filter(result, {
@@ -14,6 +14,7 @@ visible AS (
14
14
  , ke.updated_at
15
15
  , ke.attributes
16
16
  , ke.tokens AS tokens_full
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,12 @@ 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
41
44
  , CASE
42
45
  WHEN visible_fidelity IN ('full', 'summary') THEN body
43
46
  ELSE ''
44
47
  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
48
  FROM visible
59
49
  WHERE visible_fidelity IS NOT NULL
60
50
  )
@@ -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
- }