@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,17 +1,9 @@
1
1
  import { Parser } from "htmlparser2";
2
2
  import { parseEditContent } from "../plugins/hedberg/edits.js";
3
- import { normalizeAttrs } from "../plugins/hedberg/normalize.js";
3
+ import { normalizeAttrs, parseJsonEdit } from "../plugins/hedberg/normalize.js";
4
4
  import { parseSed } from "../plugins/hedberg/sed.js";
5
5
 
6
- const STORE_TOOLS = new Set([
7
- "get",
8
- "store",
9
- "rm",
10
- "set",
11
- "mv",
12
- "cp",
13
- "search",
14
- ]);
6
+ const STORE_TOOLS = new Set(["get", "rm", "set", "mv", "cp", "search"]);
15
7
  const ALL_TOOLS = new Set([
16
8
  ...STORE_TOOLS,
17
9
  "known",
@@ -51,31 +43,10 @@ function resolveCommand(name, attrs, rawBody) {
51
43
  };
52
44
  }
53
45
  }
54
- // JSON-style { search, replace } — accept valid JSON and =style variants
55
- if (trimmed.startsWith("{") && /search/.test(trimmed)) {
56
- let search = null;
57
- let replace = null;
58
- try {
59
- const json = JSON.parse(trimmed);
60
- search = json.search;
61
- replace = json.replace ?? "";
62
- } catch {
63
- // Try = style: { search="old", replace="new" }
64
- const searchMatch = trimmed.match(/search\s*=\s*"([^"]*)"/);
65
- const replaceMatch = trimmed.match(/replace\s*=\s*"([^"]*)"/);
66
- if (searchMatch) {
67
- search = searchMatch[1];
68
- replace = replaceMatch?.[1] ?? "";
69
- }
70
- }
71
- if (search != null) {
72
- return {
73
- name,
74
- path: a.path,
75
- search,
76
- replace,
77
- };
78
- }
46
+ // JSON-style { search, replace }
47
+ const jsonEdit = parseJsonEdit(trimmed);
48
+ if (jsonEdit) {
49
+ return { name, path: a.path, ...jsonEdit };
79
50
  }
80
51
  // Sed syntax: s/search/replace/flags — supports chained commands
81
52
  if (trimmed.startsWith("s/")) {
@@ -116,9 +87,9 @@ function resolveCommand(name, attrs, rawBody) {
116
87
  preview: a.preview,
117
88
  };
118
89
  }
119
- // Plain write create/overwrite
90
+ // Plain write or fidelity change
120
91
  const body = trimmed || a.body || "";
121
- return { name, path: a.path, body, preview: a.preview };
92
+ return { name, ...a, body };
122
93
  }
123
94
 
124
95
  if (name === "summarize" || name === "update" || name === "unknown") {
@@ -132,7 +103,7 @@ function resolveCommand(name, attrs, rawBody) {
132
103
  return { name, path, body };
133
104
  }
134
105
 
135
- if (name === "get" || name === "store" || name === "rm") {
106
+ if (name === "get" || name === "rm") {
136
107
  const path = a.path || trimmed || null;
137
108
  return { name, path, body: a.body, preview: a.preview };
138
109
  }
@@ -174,6 +145,9 @@ export default class XmlParser {
174
145
  static parse(content) {
175
146
  if (!content) return { commands: [], warnings: [], unparsed: "" };
176
147
 
148
+ // Normalize native tool call formats to rummy XML
149
+ const normalized = XmlParser.#normalizeToolCalls(content);
150
+
177
151
  const commands = [];
178
152
  const warnings = [];
179
153
  const textChunks = [];
@@ -190,6 +164,16 @@ export default class XmlParser {
190
164
  return;
191
165
  }
192
166
 
167
+ // Known tool opened while another is still open — close the old one.
168
+ if (current) {
169
+ warnings.push(
170
+ `Unclosed <${current.name}> before <${name}> — recovered`,
171
+ );
172
+ commands.push(
173
+ resolveCommand(current.name, current.attrs, current.rawBody),
174
+ );
175
+ }
176
+
193
177
  current = { name, attrs, rawBody: "" };
194
178
  },
195
179
 
@@ -210,6 +194,16 @@ export default class XmlParser {
210
194
  resolveCommand(current.name, current.attrs, current.rawBody),
211
195
  );
212
196
  current = null;
197
+ } else if (current && ALL_TOOLS.has(name)) {
198
+ // Mismatched close tag for a known tool — close current tag,
199
+ // don't swallow subsequent commands as body text.
200
+ warnings.push(
201
+ `Mismatched </${name}> closing <${current.name}> — recovered`,
202
+ );
203
+ commands.push(
204
+ resolveCommand(current.name, current.attrs, current.rawBody),
205
+ );
206
+ current = null;
213
207
  } else if (current) {
214
208
  current.rawBody += `</${name}>`;
215
209
  } else if (isImplied && ALL_TOOLS.has(name)) {
@@ -228,7 +222,7 @@ export default class XmlParser {
228
222
  },
229
223
  );
230
224
 
231
- parser.write(content);
225
+ parser.write(normalized);
232
226
  ended = true;
233
227
  parser.end();
234
228
 
@@ -244,4 +238,63 @@ export default class XmlParser {
244
238
  const unparsed = textChunks.join("").trim();
245
239
  return { commands, warnings, unparsed };
246
240
  }
241
+
242
+ /**
243
+ * Normalize native tool call formats to rummy XML.
244
+ * Models sometimes emit their training-format tool calls instead of
245
+ * our XML tags. The intent is unambiguous — translate silently.
246
+ */
247
+ static #normalizeToolCalls(content) {
248
+ // Gemma: ```tool_code\n<xml>...\n``` — strip code fences around valid XML
249
+ let result = content.replace(
250
+ /```(?:tool_code|tool_command|xml)\n([\s\S]*?)```/g,
251
+ (_, inner) => inner.trim(),
252
+ );
253
+
254
+ // Qwen/gemma: <|tool_call>call:NAME{key:"value"}<tool_call|>
255
+ result = result.replace(
256
+ /<\|tool_call>call:(\w+)\{([^}]*)\}<(?:tool_call\||\|tool_call)>/g,
257
+ (_, name, params) => {
258
+ if (!ALL_TOOLS.has(name)) return _;
259
+ const valueMatch = params.match(/["']([^"']+)["']/);
260
+ const body = valueMatch?.[1] || "";
261
+ return `<${name}>${body}</${name}>`;
262
+ },
263
+ );
264
+
265
+ // OpenAI function_call JSON: {"name":"search","arguments":{"query":"..."}}
266
+ result = result.replace(
267
+ /\{"name"\s*:\s*"(\w+)"\s*,\s*"arguments"\s*:\s*\{([^}]*)\}\}/g,
268
+ (_, name, args) => {
269
+ if (!ALL_TOOLS.has(name)) return _;
270
+ const pairs = [...args.matchAll(/"(\w+)"\s*:\s*"([^"]*)"/g)];
271
+ const body = pairs[0]?.[2] || "";
272
+ return `<${name}>${body}</${name}>`;
273
+ },
274
+ );
275
+
276
+ // Anthropic: <tool_use><name>search</name><input>{"query":"..."}</input></tool_use>
277
+ result = result.replace(
278
+ /<tool_use>\s*<name>(\w+)<\/name>\s*<input>\{([^}]*)\}<\/input>\s*<\/tool_use>/g,
279
+ (_, name, args) => {
280
+ if (!ALL_TOOLS.has(name)) return _;
281
+ const pairs = [...args.matchAll(/"(\w+)"\s*:\s*"([^"]*)"/g)];
282
+ const body = pairs[0]?.[2] || "";
283
+ return `<${name}>${body}</${name}>`;
284
+ },
285
+ );
286
+
287
+ // Mistral: [TOOL_CALLS] [{"name":"search","arguments":{"query":"..."}}]
288
+ result = result.replace(
289
+ /\[TOOL_CALLS\]\s*\[\{"name"\s*:\s*"(\w+)"\s*,\s*"arguments"\s*:\s*\{([^}]*)\}\}\]/g,
290
+ (_, name, args) => {
291
+ if (!ALL_TOOLS.has(name)) return _;
292
+ const pairs = [...args.matchAll(/"(\w+)"\s*:\s*"([^"]*)"/g)];
293
+ const body = pairs[0]?.[2] || "";
294
+ return `<${name}>${body}</${name}>`;
295
+ },
296
+ );
297
+
298
+ return result;
299
+ }
247
300
  }
@@ -17,25 +17,26 @@ SELECT path, body, attributes, turn
17
17
  FROM known_entries
18
18
  WHERE
19
19
  run_id = :run_id
20
- AND state = 'proposed';
20
+ AND status = 202;
21
21
 
22
22
  -- PREP: has_rejections
23
23
  SELECT COUNT(*) AS count
24
24
  FROM known_entries
25
25
  WHERE
26
26
  run_id = :run_id
27
- AND state = 'rejected';
27
+ AND loop_id = :loop_id
28
+ AND status = 403;
28
29
 
29
30
  -- PREP: has_accepted_actions
30
31
  SELECT COUNT(*) AS count
31
32
  FROM known_entries
32
33
  WHERE
33
34
  run_id = :run_id
34
- AND state = 'pass'
35
+ AND status = 200
35
36
  AND scheme IN ('set', 'sh', 'rm', 'mv', 'cp');
36
37
 
37
38
  -- PREP: get_file_entries
38
- SELECT path, state, hash, updated_at
39
+ SELECT path, status, fidelity, hash, updated_at
39
40
  FROM known_entries
40
41
  WHERE
41
42
  run_id = :run_id
@@ -1,5 +1,5 @@
1
1
  -- PREP: get_known_entries
2
- SELECT path, scheme, state, body, turn, hash, attributes
2
+ SELECT path, scheme, status, fidelity, body, turn, hash, attributes
3
3
  FROM known_entries
4
4
  WHERE run_id = :run_id
5
5
  ORDER BY path;
@@ -18,7 +18,7 @@ WHERE
18
18
  ORDER BY id;
19
19
 
20
20
  -- PREP: get_turn_audit
21
- SELECT path, scheme, state, turn, body, attributes
21
+ SELECT path, scheme, status, fidelity, turn, body, attributes
22
22
  FROM known_entries
23
23
  WHERE
24
24
  run_id = :run_id
@@ -57,12 +57,13 @@ SELECT body
57
57
  FROM known_entries
58
58
  WHERE
59
59
  run_id = :run_id
60
+ AND loop_id = :loop_id
60
61
  AND scheme = 'summarize'
61
62
  ORDER BY id DESC
62
63
  LIMIT 1;
63
64
 
64
65
  -- PREP: get_history
65
- SELECT ke.path, ke.state AS status, ke.body, ke.attributes, ke.turn
66
+ SELECT ke.path, ke.status, ke.body, ke.attributes, ke.turn
66
67
  FROM known_entries AS ke
67
68
  JOIN schemes AS s ON s.name = COALESCE(ke.scheme, 'file')
68
69
  WHERE
@@ -1,19 +1,22 @@
1
1
  -- PREP: upsert_known_entry
2
2
  INSERT INTO known_entries (
3
- run_id, turn, path, body, state, hash, attributes
4
- , tokens, tokens_full, updated_at
3
+ run_id, loop_id, turn, path, body, status, fidelity, hash
4
+ , attributes, tokens, tokens_full, updated_at
5
5
  )
6
6
  VALUES (
7
- :run_id, :turn, :path, :body, :state, :hash, COALESCE(:attributes, '{}')
7
+ :run_id, :loop_id, :turn, :path, :body, :status, :fidelity, :hash
8
+ , COALESCE(:attributes, '{}')
8
9
  , countTokens(:body)
9
10
  , countTokens(:body)
10
11
  , COALESCE(:updated_at, CURRENT_TIMESTAMP)
11
12
  )
12
13
  ON CONFLICT (run_id, path) DO UPDATE SET
13
14
  body = excluded.body
14
- , state = excluded.state
15
+ , status = excluded.status
16
+ , fidelity = excluded.fidelity
15
17
  , hash = COALESCE(excluded.hash, known_entries.hash)
16
18
  , attributes = COALESCE(excluded.attributes, known_entries.attributes)
19
+ , loop_id = excluded.loop_id
17
20
  , turn = excluded.turn
18
21
  , tokens = countTokens(excluded.body)
19
22
  , tokens_full = countTokens(excluded.body)
@@ -43,17 +46,25 @@ WHERE run_id = :run_id AND hedmatch(:pattern, path) AND scheme IS NULL;
43
46
  -- PREP: resolve_known_entry
44
47
  UPDATE known_entries
45
48
  SET
46
- state = :state
49
+ status = :status
47
50
  , body = :body
48
51
  , updated_at = CURRENT_TIMESTAMP
49
52
  WHERE run_id = :run_id AND path = :path;
50
53
 
51
- -- PREP: set_file_state
54
+ -- PREP: set_file_fidelity
52
55
  UPDATE known_entries
53
56
  SET
54
- state = :state
57
+ fidelity = :fidelity
55
58
  , tokens = CASE
56
- WHEN :state = 'summary' THEN countTokens(body)
59
+ WHEN :fidelity = 'archive'
60
+ THEN 0
61
+ WHEN :fidelity = 'index'
62
+ THEN 0
63
+ WHEN :fidelity = 'summary'
64
+ THEN COALESCE(
65
+ countTokens(json_extract(attributes, '$.summary')),
66
+ countTokens(substr(body, 1, 80))
67
+ )
57
68
  ELSE tokens_full
58
69
  END
59
70
  , updated_at = CURRENT_TIMESTAMP
@@ -62,7 +73,7 @@ WHERE run_id = :run_id AND hedmatch(:pattern, path) AND scheme IS NULL;
62
73
  -- PREP: promote_path
63
74
  UPDATE known_entries
64
75
  SET
65
- state = 'full'
76
+ fidelity = 'full'
66
77
  , turn = :turn
67
78
  , tokens = tokens_full
68
79
  , updated_at = CURRENT_TIMESTAMP
@@ -71,23 +82,42 @@ WHERE run_id = :run_id AND path = :path;
71
82
  -- PREP: demote_path
72
83
  UPDATE known_entries
73
84
  SET
74
- state = 'stored'
85
+ fidelity = 'archive'
75
86
  , tokens = 0
76
87
  , updated_at = CURRENT_TIMESTAMP
77
88
  WHERE run_id = :run_id AND path = :path;
78
89
 
90
+ -- PREP: set_fidelity
91
+ UPDATE known_entries
92
+ SET
93
+ fidelity = :fidelity
94
+ , tokens = CASE
95
+ WHEN :fidelity = 'archive'
96
+ THEN 0
97
+ WHEN :fidelity = 'index'
98
+ THEN 0
99
+ WHEN :fidelity = 'summary'
100
+ THEN COALESCE(
101
+ countTokens(json_extract(attributes, '$.summary')),
102
+ countTokens(substr(body, 1, 80))
103
+ )
104
+ ELSE countTokens(body)
105
+ END
106
+ , updated_at = CURRENT_TIMESTAMP
107
+ WHERE run_id = :run_id AND path = :path;
108
+
79
109
  -- PREP: get_entry_body
80
110
  SELECT body
81
111
  FROM known_entries
82
112
  WHERE run_id = :run_id AND path = :path;
83
113
 
84
114
  -- PREP: get_entry_state
85
- SELECT state, scheme, turn
115
+ SELECT status, fidelity, scheme, turn
86
116
  FROM known_entries
87
117
  WHERE run_id = :run_id AND path = :path;
88
118
 
89
119
  -- PREP: get_file_states_by_pattern
90
- SELECT path, state, turn
120
+ SELECT path, status, fidelity, turn
91
121
  FROM known_entries
92
122
  WHERE run_id = :run_id AND hedmatch(:pattern, path) AND scheme IS NULL
93
123
  ORDER BY path;
@@ -107,7 +137,7 @@ WHERE run_id = :run_id AND path = :path;
107
137
  -- PREP: promote_by_pattern
108
138
  UPDATE known_entries
109
139
  SET
110
- state = 'full'
140
+ fidelity = 'full'
111
141
  , turn = :turn
112
142
  , tokens = tokens_full
113
143
  , updated_at = CURRENT_TIMESTAMP
@@ -119,7 +149,7 @@ WHERE
119
149
  -- PREP: demote_by_pattern
120
150
  UPDATE known_entries
121
151
  SET
122
- state = 'stored'
152
+ fidelity = 'archive'
123
153
  , tokens = 0
124
154
  , updated_at = CURRENT_TIMESTAMP
125
155
  WHERE
@@ -128,7 +158,7 @@ WHERE
128
158
  AND (:body IS NULL OR hedsearch(:body, body));
129
159
 
130
160
  -- PREP: get_entries_by_pattern
131
- SELECT path, body, scheme, state, tokens_full, attributes
161
+ SELECT path, body, scheme, status, fidelity, tokens_full, attributes
132
162
  FROM known_entries
133
163
  WHERE
134
164
  run_id = :run_id
@@ -0,0 +1,63 @@
1
+ -- PREP: enqueue_loop
2
+ INSERT INTO loops (run_id, sequence, mode, model, prompt, config)
3
+ VALUES (:run_id, :sequence, :mode, :model, :prompt, :config)
4
+ RETURNING id, sequence;
5
+
6
+ -- PREP: next_loop
7
+ UPDATE runs
8
+ SET next_loop = next_loop + 1
9
+ WHERE id = :run_id
10
+ RETURNING next_loop - 1 AS sequence;
11
+
12
+ -- PREP: claim_next_loop
13
+ UPDATE loops
14
+ SET status = 102
15
+ WHERE
16
+ id = (
17
+ SELECT
18
+ id
19
+ FROM loops
20
+ WHERE run_id = :run_id AND status = 100
21
+ ORDER BY id
22
+ LIMIT 1
23
+ )
24
+ RETURNING id, run_id, sequence, mode, model, prompt, config;
25
+
26
+ -- PREP: complete_loop
27
+ UPDATE loops
28
+ SET status = :status, result = :result
29
+ WHERE id = :id;
30
+
31
+ -- PREP: abort_active_loop
32
+ UPDATE loops
33
+ SET status = 499
34
+ WHERE run_id = :run_id AND status = 102;
35
+
36
+ -- PREP: get_pending_loops
37
+ SELECT id, sequence, mode, model, prompt, status, created_at
38
+ FROM loops
39
+ WHERE run_id = :run_id AND status IN (100, 102)
40
+ ORDER BY id;
41
+
42
+ -- PREP: reset_active_loops
43
+ UPDATE loops
44
+ SET status = 100
45
+ WHERE status = 102;
46
+
47
+ -- PREP: get_current_loop
48
+ SELECT id, sequence, mode, model, prompt, status
49
+ FROM loops
50
+ WHERE run_id = :run_id AND status = 102
51
+ LIMIT 1;
52
+
53
+ -- PREP: get_loop_by_id
54
+ SELECT id, run_id, sequence, mode, model, prompt, status, config
55
+ FROM loops
56
+ WHERE id = :id;
57
+
58
+ -- PREP: get_latest_completed_loop
59
+ SELECT id, sequence, mode, status
60
+ FROM loops
61
+ WHERE run_id = :run_id AND status IN (200, 500)
62
+ ORDER BY id DESC
63
+ LIMIT 1;
@@ -22,14 +22,14 @@ RETURNING id;
22
22
  -- PREP: get_run_by_alias
23
23
  SELECT
24
24
  id, project_id, parent_run_id, model, status, alias
25
- , temperature, persona, context_limit, next_turn, created_at
25
+ , temperature, persona, context_limit, next_turn, next_loop, created_at
26
26
  FROM runs
27
27
  WHERE alias = :alias;
28
28
 
29
29
  -- PREP: get_run_by_id
30
30
  SELECT
31
31
  id, project_id, parent_run_id, model, status, alias
32
- , temperature, persona, context_limit, next_turn, created_at
32
+ , temperature, persona, context_limit, next_turn, next_loop, created_at
33
33
  FROM runs
34
34
  WHERE id = :id;
35
35
 
@@ -80,11 +80,11 @@ RETURNING next_turn - 1 AS turn;
80
80
 
81
81
  -- PREP: fork_known_entries
82
82
  INSERT INTO known_entries (
83
- run_id, turn, path, body, state
83
+ run_id, loop_id, turn, path, body, status, fidelity
84
84
  , hash, attributes, tokens, tokens_full, refs, write_count
85
85
  )
86
86
  SELECT
87
- :new_run_id, turn, path, body, state
87
+ :new_run_id, NULL, turn, path, body, status, fidelity
88
88
  , hash, attributes, tokens, tokens_full, refs, write_count
89
89
  FROM known_entries
90
90
  WHERE run_id = :parent_run_id;
@@ -94,7 +94,7 @@ SELECT r.id
94
94
  FROM runs AS r
95
95
  WHERE
96
96
  r.project_id = :project_id
97
- AND r.status IN ('queued', 'running', 'proposed');
97
+ AND r.status IN (100, 102, 202);
98
98
 
99
99
  -- PREP: get_latest_run
100
100
  SELECT r.id
@@ -110,5 +110,5 @@ WHERE r.project_id = :project_id;
110
110
 
111
111
  -- PREP: abort_stuck_runs
112
112
  UPDATE runs
113
- SET status = 'aborted'
114
- WHERE status IN ('running', 'queued');
113
+ SET status = 499
114
+ WHERE status IN (100, 102);
@@ -1,3 +1,6 @@
1
1
  -- PREP: upsert_scheme
2
- INSERT OR REPLACE INTO schemes (name, fidelity, model_visible, valid_states, category)
3
- VALUES (:name, :fidelity, :model_visible, :valid_states, :category);
2
+ INSERT OR REPLACE INTO schemes (name, model_visible, category)
3
+ VALUES (:name, :model_visible, :category);
4
+
5
+ -- PREP: get_all_schemes
6
+ SELECT name, model_visible, category FROM schemes;
@@ -1,28 +1,13 @@
1
1
  /**
2
- * Token counting with tiktoken (o200k_base) and simple fallback.
3
- * o200k_base is the tokenizer for GPT-4o and newer OpenAI models.
4
- * Better multilingual and code handling than cl100k_base.
5
- * Exact counts vary by model tokenizer these are for budgeting, not billing.
2
+ * Token estimation. Conservative character-based approximation.
3
+ * RUMMY_TOKEN_DIVISOR controls characters per token.
4
+ * No external dependencies. The budget contract is exact.
5
+ * contextSize is the ceiling. countTokens is the measurement.
6
6
  */
7
7
 
8
- let encoder = null;
9
-
10
- try {
11
- const tiktoken = await import("tiktoken");
12
- encoder = tiktoken.get_encoding("o200k_base");
13
- } catch {
14
- // tiktoken unavailable — use character-based estimate
15
- }
8
+ const DIVISOR = Number(process.env.RUMMY_TOKEN_DIVISOR);
16
9
 
17
10
  export function countTokens(text) {
18
11
  if (!text) return 0;
19
- if (encoder) {
20
- try {
21
- const tokens = encoder.encode(text);
22
- return tokens.length;
23
- } catch {
24
- // Fallback on encoding error
25
- }
26
- }
27
- return Math.ceil(text.length / 4);
12
+ return Math.ceil(text.length / DIVISOR);
28
13
  }
@@ -1,12 +1,14 @@
1
1
  -- PREP: create_turn
2
- INSERT INTO turns (run_id, sequence)
3
- VALUES (:run_id, :sequence)
2
+ INSERT INTO turns (run_id, loop_id, sequence)
3
+ VALUES (:run_id, :loop_id, :sequence)
4
4
  RETURNING id, sequence;
5
5
 
6
6
  -- PREP: update_turn_stats
7
7
  UPDATE turns
8
8
  SET
9
- prompt_tokens = :prompt_tokens
9
+ context_tokens = :context_tokens
10
+ , reasoning_content = :reasoning_content
11
+ , prompt_tokens = :prompt_tokens
10
12
  , cached_tokens = :cached_tokens
11
13
  , completion_tokens = :completion_tokens
12
14
  , reasoning_tokens = :reasoning_tokens
@@ -25,8 +27,15 @@ SELECT
25
27
  FROM turns
26
28
  WHERE run_id = :run_id;
27
29
 
30
+ -- PREP: get_last_context_tokens
31
+ SELECT context_tokens
32
+ FROM turns
33
+ WHERE run_id = :run_id AND context_tokens > 0
34
+ ORDER BY sequence DESC
35
+ LIMIT 1;
36
+
28
37
  -- PREP: get_run_log
29
- SELECT ke.path, ke.state AS status, ke.body, ke.attributes
38
+ SELECT ke.path, ke.status, ke.body, ke.attributes
30
39
  FROM known_entries AS ke
31
40
  JOIN schemes AS s ON s.name = COALESCE(ke.scheme, 'file')
32
41
  WHERE
@@ -39,6 +39,7 @@ export default function createHooks(debug = false) {
39
39
  },
40
40
  },
41
41
  run: {
42
+ created: createEvent("run.created"),
42
43
  started: createEvent("run.started"),
43
44
  progress: createEvent("run.progress"),
44
45
  state: createEvent("run.state"),
@@ -47,10 +48,15 @@ export default function createHooks(debug = false) {
47
48
  completed: createEvent("run.step.completed"),
48
49
  },
49
50
  },
51
+ loop: {
52
+ started: createEvent("loop.started"),
53
+ completed: createEvent("loop.completed"),
54
+ },
50
55
  turn: {
51
56
  started: createEvent("turn.started"),
52
57
  response: createEvent("turn.response"),
53
58
  proposing: createEvent("turn.proposing"),
59
+ completed: createEvent("turn.completed"),
54
60
  },
55
61
  assembly: {
56
62
  system: createFilter("assembly.system"),
@@ -67,6 +73,10 @@ export default function createHooks(debug = false) {
67
73
  started: createEvent("act.started"),
68
74
  completed: createEvent("act.completed"),
69
75
  },
76
+ panic: {
77
+ started: createEvent("panic.started"),
78
+ completed: createEvent("panic.completed"),
79
+ },
70
80
  llm: {
71
81
  request: {
72
82
  started: createEvent("llm.request.started"),
@@ -80,9 +90,17 @@ export default function createHooks(debug = false) {
80
90
  tools: createFilter("prompt.tools"),
81
91
  },
82
92
  entry: {
93
+ recording: createFilter("entry.recording"),
83
94
  created: createEvent("entry.created"),
84
95
  changed: createEvent("entry.changed"),
85
96
  },
97
+ tool: {
98
+ before: createEvent("tool.before"),
99
+ after: createEvent("tool.after"),
100
+ },
101
+ context: {
102
+ materialized: createEvent("context.materialized"),
103
+ },
86
104
  action: {},
87
105
  ui: {
88
106
  render: createEvent("ui.render"),
@@ -48,22 +48,27 @@ export default class PluginContext {
48
48
  return this.#schemes;
49
49
  }
50
50
 
51
- registerScheme({
52
- name,
53
- fidelity = "full",
54
- modelVisible = 1,
55
- validStates = ["full", "proposed", "pass", "rejected", "error"],
56
- category = "result",
57
- } = {}) {
51
+ registerScheme({ name, modelVisible = 1, category = "logging" } = {}) {
52
+ if (!PluginContext.CATEGORIES.has(category)) {
53
+ throw new Error(
54
+ `Invalid category "${category}". Must be one of: ${[...PluginContext.CATEGORIES].join(", ")}`,
55
+ );
56
+ }
58
57
  this.#schemes.push({
59
58
  name: name || this.#name,
60
- fidelity,
61
59
  model_visible: modelVisible,
62
- valid_states: JSON.stringify(validStates),
63
60
  category,
64
61
  });
65
62
  }
66
63
 
64
+ static CATEGORIES = Object.freeze(
65
+ new Set(["data", "logging", "unknown", "prompt"]),
66
+ );
67
+
68
+ ensureTool() {
69
+ this.#hooks.tools.ensureTool(this.#name);
70
+ }
71
+
67
72
  /**
68
73
  * Register a named callback for this plugin.
69
74
  * "handler" registers the tool handler.
@@ -78,7 +83,6 @@ export default class PluginContext {
78
83
  return;
79
84
  }
80
85
  if (event === "full" || event === "summary") {
81
- this.#hooks.tools.ensureTool(this.#name);
82
86
  this.#hooks.tools.onView(this.#name, callback, event);
83
87
  return;
84
88
  }