@possumtech/rummy 0.2.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 (120) hide show
  1. package/.env.example +55 -0
  2. package/LICENSE +21 -0
  3. package/PLUGINS.md +302 -0
  4. package/README.md +41 -0
  5. package/SPEC.md +524 -0
  6. package/lang/en.json +34 -0
  7. package/migrations/001_initial_schema.sql +226 -0
  8. package/package.json +54 -0
  9. package/service.js +143 -0
  10. package/src/agent/AgentLoop.js +553 -0
  11. package/src/agent/ContextAssembler.js +29 -0
  12. package/src/agent/KnownStore.js +254 -0
  13. package/src/agent/ProjectAgent.js +101 -0
  14. package/src/agent/ResponseHealer.js +134 -0
  15. package/src/agent/TurnExecutor.js +457 -0
  16. package/src/agent/XmlParser.js +247 -0
  17. package/src/agent/known_checks.sql +42 -0
  18. package/src/agent/known_queries.sql +80 -0
  19. package/src/agent/known_store.sql +161 -0
  20. package/src/agent/messages.js +17 -0
  21. package/src/agent/prompt_queue.sql +39 -0
  22. package/src/agent/runs.sql +114 -0
  23. package/src/agent/schemes.sql +3 -0
  24. package/src/agent/sessions.sql +51 -0
  25. package/src/agent/tokens.js +28 -0
  26. package/src/agent/turns.sql +36 -0
  27. package/src/hooks/HookRegistry.js +72 -0
  28. package/src/hooks/Hooks.js +115 -0
  29. package/src/hooks/PluginContext.js +116 -0
  30. package/src/hooks/RummyContext.js +181 -0
  31. package/src/hooks/ToolRegistry.js +83 -0
  32. package/src/llm/LlmProvider.js +107 -0
  33. package/src/llm/OllamaClient.js +88 -0
  34. package/src/llm/OpenAiClient.js +80 -0
  35. package/src/llm/OpenRouterClient.js +78 -0
  36. package/src/llm/XaiClient.js +113 -0
  37. package/src/plugins/ask_user/README.md +18 -0
  38. package/src/plugins/ask_user/ask_user.js +48 -0
  39. package/src/plugins/ask_user/docs.md +2 -0
  40. package/src/plugins/cp/README.md +18 -0
  41. package/src/plugins/cp/cp.js +55 -0
  42. package/src/plugins/cp/docs.md +2 -0
  43. package/src/plugins/current/README.md +14 -0
  44. package/src/plugins/current/current.js +48 -0
  45. package/src/plugins/engine/README.md +12 -0
  46. package/src/plugins/engine/engine.sql +18 -0
  47. package/src/plugins/engine/turn_context.sql +51 -0
  48. package/src/plugins/env/README.md +14 -0
  49. package/src/plugins/env/docs.md +2 -0
  50. package/src/plugins/env/env.js +32 -0
  51. package/src/plugins/file/README.md +25 -0
  52. package/src/plugins/file/file.js +85 -0
  53. package/src/plugins/get/README.md +19 -0
  54. package/src/plugins/get/docs.md +6 -0
  55. package/src/plugins/get/get.js +53 -0
  56. package/src/plugins/hedberg/README.md +72 -0
  57. package/src/plugins/hedberg/docs.md +9 -0
  58. package/src/plugins/hedberg/edits.js +65 -0
  59. package/src/plugins/hedberg/hedberg.js +89 -0
  60. package/src/plugins/hedberg/matcher.js +181 -0
  61. package/src/plugins/hedberg/normalize.js +41 -0
  62. package/src/plugins/hedberg/patterns.js +452 -0
  63. package/src/plugins/hedberg/sed.js +48 -0
  64. package/src/plugins/helpers.js +22 -0
  65. package/src/plugins/index.js +180 -0
  66. package/src/plugins/instructions/README.md +11 -0
  67. package/src/plugins/instructions/instructions.js +37 -0
  68. package/src/plugins/instructions/preamble.md +12 -0
  69. package/src/plugins/known/README.md +18 -0
  70. package/src/plugins/known/docs.md +3 -0
  71. package/src/plugins/known/known.js +57 -0
  72. package/src/plugins/mv/README.md +18 -0
  73. package/src/plugins/mv/docs.md +2 -0
  74. package/src/plugins/mv/mv.js +56 -0
  75. package/src/plugins/previous/README.md +15 -0
  76. package/src/plugins/previous/previous.js +50 -0
  77. package/src/plugins/progress/README.md +17 -0
  78. package/src/plugins/progress/progress.js +44 -0
  79. package/src/plugins/prompt/README.md +16 -0
  80. package/src/plugins/prompt/prompt.js +45 -0
  81. package/src/plugins/rm/README.md +18 -0
  82. package/src/plugins/rm/docs.md +4 -0
  83. package/src/plugins/rm/rm.js +51 -0
  84. package/src/plugins/rpc/README.md +45 -0
  85. package/src/plugins/rpc/rpc.js +587 -0
  86. package/src/plugins/set/README.md +32 -0
  87. package/src/plugins/set/docs.md +4 -0
  88. package/src/plugins/set/set.js +268 -0
  89. package/src/plugins/sh/README.md +18 -0
  90. package/src/plugins/sh/docs.md +2 -0
  91. package/src/plugins/sh/sh.js +32 -0
  92. package/src/plugins/skills/README.md +25 -0
  93. package/src/plugins/skills/skills.js +175 -0
  94. package/src/plugins/store/README.md +20 -0
  95. package/src/plugins/store/docs.md +5 -0
  96. package/src/plugins/store/store.js +52 -0
  97. package/src/plugins/summarize/README.md +18 -0
  98. package/src/plugins/summarize/docs.md +4 -0
  99. package/src/plugins/summarize/summarize.js +24 -0
  100. package/src/plugins/telemetry/README.md +19 -0
  101. package/src/plugins/telemetry/rpc_log.sql +28 -0
  102. package/src/plugins/telemetry/telemetry.js +186 -0
  103. package/src/plugins/unknown/README.md +23 -0
  104. package/src/plugins/unknown/docs.md +5 -0
  105. package/src/plugins/unknown/unknown.js +31 -0
  106. package/src/plugins/update/README.md +18 -0
  107. package/src/plugins/update/docs.md +4 -0
  108. package/src/plugins/update/update.js +24 -0
  109. package/src/server/ClientConnection.js +228 -0
  110. package/src/server/RpcRegistry.js +52 -0
  111. package/src/server/SocketServer.js +43 -0
  112. package/src/sql/file_constraints.sql +15 -0
  113. package/src/sql/functions/countTokens.js +7 -0
  114. package/src/sql/functions/hedmatch.js +8 -0
  115. package/src/sql/functions/hedreplace.js +8 -0
  116. package/src/sql/functions/hedsearch.js +8 -0
  117. package/src/sql/functions/schemeOf.js +7 -0
  118. package/src/sql/functions/slugify.js +6 -0
  119. package/src/sql/v_model_context.sql +101 -0
  120. package/src/sql/v_run_log.sql +23 -0
@@ -0,0 +1,42 @@
1
+ -- PREP: count_unknowns
2
+ SELECT COUNT(*) AS count
3
+ FROM known_entries
4
+ WHERE
5
+ run_id = :run_id
6
+ AND scheme = 'unknown';
7
+
8
+ -- PREP: get_unknown_values
9
+ SELECT body
10
+ FROM known_entries
11
+ WHERE
12
+ run_id = :run_id
13
+ AND scheme = 'unknown';
14
+
15
+ -- PREP: get_unresolved
16
+ SELECT path, body, attributes, turn
17
+ FROM known_entries
18
+ WHERE
19
+ run_id = :run_id
20
+ AND state = 'proposed';
21
+
22
+ -- PREP: has_rejections
23
+ SELECT COUNT(*) AS count
24
+ FROM known_entries
25
+ WHERE
26
+ run_id = :run_id
27
+ AND state = 'rejected';
28
+
29
+ -- PREP: has_accepted_actions
30
+ SELECT COUNT(*) AS count
31
+ FROM known_entries
32
+ WHERE
33
+ run_id = :run_id
34
+ AND state = 'pass'
35
+ AND scheme IN ('set', 'sh', 'rm', 'mv', 'cp');
36
+
37
+ -- PREP: get_file_entries
38
+ SELECT path, state, hash, updated_at
39
+ FROM known_entries
40
+ WHERE
41
+ run_id = :run_id
42
+ AND scheme IS NULL;
@@ -0,0 +1,80 @@
1
+ -- PREP: get_known_entries
2
+ SELECT path, scheme, state, body, turn, hash, attributes
3
+ FROM known_entries
4
+ WHERE run_id = :run_id
5
+ ORDER BY path;
6
+
7
+ -- PREP: get_results
8
+ SELECT tool, target, status, path, body
9
+ FROM v_run_log
10
+ WHERE run_id = :run_id;
11
+
12
+ -- PREP: get_unknowns
13
+ SELECT path, body
14
+ FROM known_entries
15
+ WHERE
16
+ run_id = :run_id
17
+ AND scheme = 'unknown'
18
+ ORDER BY id;
19
+
20
+ -- PREP: get_turn_audit
21
+ SELECT path, scheme, state, turn, body, attributes
22
+ FROM known_entries
23
+ WHERE
24
+ run_id = :run_id
25
+ AND turn = :turn
26
+ ORDER BY id;
27
+
28
+ -- PREP: get_reasoning
29
+ SELECT path, body, turn
30
+ FROM known_entries
31
+ WHERE
32
+ run_id = :run_id
33
+ AND scheme = 'reasoning'
34
+ ORDER BY id;
35
+
36
+ -- PREP: get_latest_user_prompt
37
+ SELECT body
38
+ FROM known_entries
39
+ WHERE
40
+ run_id = :run_id
41
+ AND scheme IN ('ask', 'act')
42
+ AND body != ''
43
+ ORDER BY id DESC
44
+ LIMIT 1;
45
+
46
+ -- PREP: get_latest_prompt
47
+ SELECT path, scheme, body, attributes
48
+ FROM known_entries
49
+ WHERE
50
+ run_id = :run_id
51
+ AND scheme = 'prompt'
52
+ ORDER BY id DESC
53
+ LIMIT 1;
54
+
55
+ -- PREP: get_latest_summary
56
+ SELECT body
57
+ FROM known_entries
58
+ WHERE
59
+ run_id = :run_id
60
+ AND scheme = 'summarize'
61
+ ORDER BY id DESC
62
+ LIMIT 1;
63
+
64
+ -- PREP: get_history
65
+ SELECT ke.path, ke.state AS status, ke.body, ke.attributes, ke.turn
66
+ FROM known_entries AS ke
67
+ JOIN schemes AS s ON s.name = COALESCE(ke.scheme, 'file')
68
+ WHERE
69
+ ke.run_id = :run_id
70
+ AND ke.scheme IS NOT NULL
71
+ AND s.category NOT IN ('knowledge', 'file', 'audit')
72
+ ORDER BY ke.id;
73
+
74
+ -- PREP: get_content
75
+ SELECT path, body, turn
76
+ FROM known_entries
77
+ WHERE
78
+ run_id = :run_id
79
+ AND scheme = 'content'
80
+ ORDER BY id;
@@ -0,0 +1,161 @@
1
+ -- PREP: upsert_known_entry
2
+ INSERT INTO known_entries (
3
+ run_id, turn, path, body, state, hash, attributes
4
+ , tokens, tokens_full, updated_at
5
+ )
6
+ VALUES (
7
+ :run_id, :turn, :path, :body, :state, :hash, COALESCE(:attributes, '{}')
8
+ , countTokens(:body)
9
+ , countTokens(:body)
10
+ , COALESCE(:updated_at, CURRENT_TIMESTAMP)
11
+ )
12
+ ON CONFLICT (run_id, path) DO UPDATE SET
13
+ body = excluded.body
14
+ , state = excluded.state
15
+ , hash = COALESCE(excluded.hash, known_entries.hash)
16
+ , attributes = COALESCE(excluded.attributes, known_entries.attributes)
17
+ , turn = excluded.turn
18
+ , tokens = countTokens(excluded.body)
19
+ , tokens_full = countTokens(excluded.body)
20
+ , write_count = known_entries.write_count + 1
21
+ , updated_at = COALESCE(excluded.updated_at, CURRENT_TIMESTAMP);
22
+
23
+ -- PREP: recount_tokens
24
+ UPDATE known_entries
25
+ SET tokens = :tokens, tokens_full = :tokens
26
+ WHERE run_id = :run_id AND path = :path;
27
+
28
+ -- PREP: get_stale_tokens
29
+ SELECT path, body
30
+ FROM known_entries
31
+ WHERE
32
+ run_id = :run_id
33
+ AND turn = :turn;
34
+
35
+ -- PREP: delete_known_entry
36
+ DELETE FROM known_entries
37
+ WHERE run_id = :run_id AND path = :path;
38
+
39
+ -- PREP: delete_file_entries_by_pattern
40
+ DELETE FROM known_entries
41
+ WHERE run_id = :run_id AND hedmatch(:pattern, path) AND scheme IS NULL;
42
+
43
+ -- PREP: resolve_known_entry
44
+ UPDATE known_entries
45
+ SET
46
+ state = :state
47
+ , body = :body
48
+ , updated_at = CURRENT_TIMESTAMP
49
+ WHERE run_id = :run_id AND path = :path;
50
+
51
+ -- PREP: set_file_state
52
+ UPDATE known_entries
53
+ SET
54
+ state = :state
55
+ , tokens = CASE
56
+ WHEN :state = 'summary' THEN countTokens(body)
57
+ ELSE tokens_full
58
+ END
59
+ , updated_at = CURRENT_TIMESTAMP
60
+ WHERE run_id = :run_id AND hedmatch(:pattern, path) AND scheme IS NULL;
61
+
62
+ -- PREP: promote_path
63
+ UPDATE known_entries
64
+ SET
65
+ state = 'full'
66
+ , turn = :turn
67
+ , tokens = tokens_full
68
+ , updated_at = CURRENT_TIMESTAMP
69
+ WHERE run_id = :run_id AND path = :path;
70
+
71
+ -- PREP: demote_path
72
+ UPDATE known_entries
73
+ SET
74
+ state = 'stored'
75
+ , tokens = 0
76
+ , updated_at = CURRENT_TIMESTAMP
77
+ WHERE run_id = :run_id AND path = :path;
78
+
79
+ -- PREP: get_entry_body
80
+ SELECT body
81
+ FROM known_entries
82
+ WHERE run_id = :run_id AND path = :path;
83
+
84
+ -- PREP: get_entry_state
85
+ SELECT state, scheme, turn
86
+ FROM known_entries
87
+ WHERE run_id = :run_id AND path = :path;
88
+
89
+ -- PREP: get_file_states_by_pattern
90
+ SELECT path, state, turn
91
+ FROM known_entries
92
+ WHERE run_id = :run_id AND hedmatch(:pattern, path) AND scheme IS NULL
93
+ ORDER BY path;
94
+
95
+ -- PREP: update_entry_attributes
96
+ UPDATE known_entries
97
+ SET
98
+ attributes = json_patch(attributes, :attributes)
99
+ , updated_at = CURRENT_TIMESTAMP
100
+ WHERE run_id = :run_id AND path = :path;
101
+
102
+ -- PREP: get_entry_attributes
103
+ SELECT attributes
104
+ FROM known_entries
105
+ WHERE run_id = :run_id AND path = :path;
106
+
107
+ -- PREP: promote_by_pattern
108
+ UPDATE known_entries
109
+ SET
110
+ state = 'full'
111
+ , turn = :turn
112
+ , tokens = tokens_full
113
+ , updated_at = CURRENT_TIMESTAMP
114
+ WHERE
115
+ run_id = :run_id
116
+ AND hedmatch(:path, path)
117
+ AND (:body IS NULL OR hedsearch(:body, body));
118
+
119
+ -- PREP: demote_by_pattern
120
+ UPDATE known_entries
121
+ SET
122
+ state = 'stored'
123
+ , tokens = 0
124
+ , updated_at = CURRENT_TIMESTAMP
125
+ WHERE
126
+ run_id = :run_id
127
+ AND hedmatch(:path, path)
128
+ AND (:body IS NULL OR hedsearch(:body, body));
129
+
130
+ -- PREP: get_entries_by_pattern
131
+ SELECT path, body, scheme, state, tokens_full, attributes
132
+ FROM known_entries
133
+ WHERE
134
+ run_id = :run_id
135
+ AND hedmatch(:path, path)
136
+ AND (:body IS NULL OR hedsearch(:body, body))
137
+ ORDER BY path
138
+ LIMIT
139
+ COALESCE(:limit, -1)
140
+ OFFSET
141
+ COALESCE(:offset, 0);
142
+
143
+ -- PREP: delete_entries_by_pattern
144
+ DELETE FROM known_entries
145
+ WHERE
146
+ run_id = :run_id
147
+ AND hedmatch(:path, path)
148
+ AND (:body IS NULL OR hedsearch(:body, body));
149
+
150
+ -- PREP: update_body_by_pattern
151
+ UPDATE known_entries
152
+ SET
153
+ body = :new_body
154
+ , tokens = countTokens(:new_body)
155
+ , tokens_full = countTokens(:new_body)
156
+ , write_count = write_count + 1
157
+ , updated_at = CURRENT_TIMESTAMP
158
+ WHERE
159
+ run_id = :run_id
160
+ AND hedmatch(:path, path)
161
+ AND (:body IS NULL OR hedsearch(:body, body));
@@ -0,0 +1,17 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ const __dirname = dirname(fileURLToPath(import.meta.url));
6
+ const catalog = JSON.parse(
7
+ readFileSync(join(__dirname, "../../lang/en.json"), "utf8"),
8
+ );
9
+
10
+ export default function msg(key, params = {}) {
11
+ const template = catalog[key];
12
+ if (!template) throw new Error(`Missing message key: ${key}`);
13
+ return template.replace(/\{(\w+)\}/g, (_, name) => {
14
+ if (name in params) return String(params[name]);
15
+ return `{${name}}`;
16
+ });
17
+ }
@@ -0,0 +1,39 @@
1
+ -- PREP: enqueue_prompt
2
+ INSERT INTO prompt_queue (run_id, mode, model, prompt, config)
3
+ VALUES (:run_id, :mode, :model, :prompt, :config)
4
+ RETURNING id;
5
+
6
+ -- PREP: claim_next_prompt
7
+ UPDATE prompt_queue
8
+ SET status = 'active'
9
+ WHERE
10
+ id = (
11
+ SELECT
12
+ id
13
+ FROM prompt_queue
14
+ WHERE run_id = :run_id AND status = 'pending'
15
+ ORDER BY id
16
+ LIMIT 1
17
+ )
18
+ RETURNING id, run_id, mode, model, prompt, config;
19
+
20
+ -- PREP: complete_prompt
21
+ UPDATE prompt_queue
22
+ SET status = 'completed', result = :result
23
+ WHERE id = :id;
24
+
25
+ -- PREP: abort_active_prompt
26
+ UPDATE prompt_queue
27
+ SET status = 'aborted'
28
+ WHERE run_id = :run_id AND status = 'active';
29
+
30
+ -- PREP: get_pending_prompts
31
+ SELECT id, mode, model, prompt, status, created_at
32
+ FROM prompt_queue
33
+ WHERE run_id = :run_id AND status IN ('pending', 'active')
34
+ ORDER BY id;
35
+
36
+ -- PREP: reset_active_prompts
37
+ UPDATE prompt_queue
38
+ SET status = 'pending'
39
+ WHERE status = 'active';
@@ -0,0 +1,114 @@
1
+ -- PREP: create_run
2
+ INSERT INTO runs (
3
+ project_id
4
+ , parent_run_id
5
+ , model
6
+ , alias
7
+ , temperature
8
+ , persona
9
+ , context_limit
10
+ )
11
+ VALUES (
12
+ :project_id
13
+ , :parent_run_id
14
+ , :model
15
+ , :alias
16
+ , :temperature
17
+ , :persona
18
+ , :context_limit
19
+ )
20
+ RETURNING id;
21
+
22
+ -- PREP: get_run_by_alias
23
+ SELECT
24
+ id, project_id, parent_run_id, model, status, alias
25
+ , temperature, persona, context_limit, next_turn, created_at
26
+ FROM runs
27
+ WHERE alias = :alias;
28
+
29
+ -- PREP: get_run_by_id
30
+ SELECT
31
+ id, project_id, parent_run_id, model, status, alias
32
+ , temperature, persona, context_limit, next_turn, created_at
33
+ FROM runs
34
+ WHERE id = :id;
35
+
36
+ -- PREP: get_runs_by_project
37
+ SELECT
38
+ r.alias
39
+ , r.status
40
+ , r.created_at
41
+ , r.next_turn - 1 AS turn
42
+ , (
43
+ SELECT ke.body
44
+ FROM known_entries AS ke
45
+ WHERE
46
+ ke.run_id = r.id
47
+ AND ke.scheme = 'summarize'
48
+ ORDER BY ke.id DESC
49
+ LIMIT 1
50
+ ) AS summary
51
+ FROM runs AS r
52
+ WHERE r.project_id = :project_id
53
+ ORDER BY r.created_at DESC
54
+ LIMIT
55
+ COALESCE(:limit, -1)
56
+ OFFSET
57
+ COALESCE(:offset, 0);
58
+
59
+ -- PREP: rename_run
60
+ UPDATE runs
61
+ SET alias = :new_alias
62
+ WHERE id = :id AND alias = :old_alias;
63
+
64
+ -- PREP: update_run_status
65
+ UPDATE runs SET status = :status WHERE id = :id;
66
+
67
+ -- PREP: update_run_config
68
+ UPDATE runs SET
69
+ temperature = COALESCE(:temperature, temperature)
70
+ , persona = COALESCE(:persona, persona)
71
+ , context_limit = COALESCE(:context_limit, context_limit)
72
+ , model = COALESCE(:model, model)
73
+ WHERE id = :id;
74
+
75
+ -- PREP: next_turn
76
+ UPDATE runs
77
+ SET next_turn = next_turn + 1
78
+ WHERE id = :run_id
79
+ RETURNING next_turn - 1 AS turn;
80
+
81
+ -- PREP: fork_known_entries
82
+ INSERT INTO known_entries (
83
+ run_id, turn, path, body, state
84
+ , hash, attributes, tokens, tokens_full, refs, write_count
85
+ )
86
+ SELECT
87
+ :new_run_id, turn, path, body, state
88
+ , hash, attributes, tokens, tokens_full, refs, write_count
89
+ FROM known_entries
90
+ WHERE run_id = :parent_run_id;
91
+
92
+ -- PREP: get_active_runs
93
+ SELECT r.id
94
+ FROM runs AS r
95
+ WHERE
96
+ r.project_id = :project_id
97
+ AND r.status IN ('queued', 'running', 'proposed');
98
+
99
+ -- PREP: get_latest_run
100
+ SELECT r.id
101
+ FROM runs AS r
102
+ WHERE r.project_id = :project_id
103
+ ORDER BY r.created_at DESC
104
+ LIMIT 1;
105
+
106
+ -- PREP: get_all_runs
107
+ SELECT r.id
108
+ FROM runs AS r
109
+ WHERE r.project_id = :project_id;
110
+
111
+ -- PREP: abort_stuck_runs
112
+ UPDATE runs
113
+ SET status = 'aborted'
114
+ WHERE status IN ('running', 'queued');
@@ -0,0 +1,3 @@
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);
@@ -0,0 +1,51 @@
1
+ -- PREP: upsert_project
2
+ INSERT INTO projects (name, project_root, config_path)
3
+ VALUES (:name, :project_root, :config_path)
4
+ ON CONFLICT (name) DO UPDATE SET
5
+ project_root = COALESCE(excluded.project_root, projects.project_root)
6
+ , config_path = COALESCE(excluded.config_path, projects.config_path)
7
+ RETURNING id;
8
+
9
+ -- PREP: get_project_by_id
10
+ SELECT id, name, project_root, config_path, created_at
11
+ FROM projects
12
+ WHERE id = :id;
13
+
14
+ -- PREP: get_project_by_name
15
+ SELECT id, name, project_root, config_path, created_at
16
+ FROM projects
17
+ WHERE name = :name;
18
+
19
+ -- PREP: upsert_model
20
+ INSERT INTO models (alias, actual, context_length)
21
+ VALUES (:alias, :actual, :context_length)
22
+ ON CONFLICT (alias) DO UPDATE SET
23
+ actual = excluded.actual
24
+ , context_length = COALESCE(excluded.context_length, models.context_length)
25
+ RETURNING id;
26
+
27
+ -- PREP: get_model_by_alias
28
+ SELECT id, alias, actual, context_length
29
+ FROM models
30
+ WHERE alias = :alias;
31
+
32
+ -- PREP: get_models
33
+ SELECT id, alias, actual, context_length
34
+ FROM models
35
+ ORDER BY alias
36
+ LIMIT
37
+ COALESCE(:limit, -1)
38
+ OFFSET
39
+ COALESCE(:offset, 0);
40
+
41
+ -- PREP: update_model_context_length
42
+ UPDATE models SET context_length = :context_length WHERE alias = :alias;
43
+
44
+ -- PREP: delete_model
45
+ DELETE FROM models WHERE alias = :alias;
46
+
47
+ -- PREP: purge_old_runs
48
+ DELETE FROM runs
49
+ WHERE
50
+ status IN ('completed', 'aborted')
51
+ AND created_at < datetime('now', '-' || :retention_days || ' days');
@@ -0,0 +1,28 @@
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.
6
+ */
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
+ }
16
+
17
+ export function countTokens(text) {
18
+ 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);
28
+ }
@@ -0,0 +1,36 @@
1
+ -- PREP: create_turn
2
+ INSERT INTO turns (run_id, sequence)
3
+ VALUES (:run_id, :sequence)
4
+ RETURNING id, sequence;
5
+
6
+ -- PREP: update_turn_stats
7
+ UPDATE turns
8
+ SET
9
+ prompt_tokens = :prompt_tokens
10
+ , cached_tokens = :cached_tokens
11
+ , completion_tokens = :completion_tokens
12
+ , reasoning_tokens = :reasoning_tokens
13
+ , total_tokens = :total_tokens
14
+ , cost = :cost
15
+ WHERE id = :id;
16
+
17
+ -- PREP: get_run_usage
18
+ SELECT
19
+ COALESCE(SUM(prompt_tokens), 0) AS prompt_tokens
20
+ , COALESCE(SUM(cached_tokens), 0) AS cached_tokens
21
+ , COALESCE(SUM(completion_tokens), 0) AS completion_tokens
22
+ , COALESCE(SUM(reasoning_tokens), 0) AS reasoning_tokens
23
+ , COALESCE(SUM(total_tokens), 0) AS total_tokens
24
+ , COALESCE(SUM(cost), 0) AS cost
25
+ FROM turns
26
+ WHERE run_id = :run_id;
27
+
28
+ -- PREP: get_run_log
29
+ SELECT ke.path, ke.state AS status, ke.body, ke.attributes
30
+ FROM known_entries AS ke
31
+ JOIN schemes AS s ON s.name = COALESCE(ke.scheme, 'file')
32
+ WHERE
33
+ ke.run_id = :run_id
34
+ AND ke.scheme IS NOT NULL
35
+ AND s.category NOT IN ('knowledge')
36
+ ORDER BY ke.id;
@@ -0,0 +1,72 @@
1
+ /**
2
+ * HookRegistry manages a simple, priority-ordered pipeline of processors.
3
+ * It also supports basic event emitters for side-effects.
4
+ */
5
+ export default class HookRegistry {
6
+ #processors = [];
7
+ #events = new Map();
8
+ #filters = new Map();
9
+ #debug;
10
+
11
+ constructor(debug = false) {
12
+ this.#debug = debug;
13
+ }
14
+
15
+ /**
16
+ * Register a processor for the Turn XML Document.
17
+ */
18
+ onTurn(callback, priority = 10) {
19
+ this.#processors.push({ callback, priority });
20
+ this.#processors.sort((a, b) => a.priority - b.priority);
21
+ }
22
+
23
+ /**
24
+ * Run all registered Turn processors.
25
+ */
26
+ async processTurn(rummy) {
27
+ for (const p of this.#processors) {
28
+ const start = performance.now();
29
+ await p.callback(rummy);
30
+ if (this.#debug) {
31
+ const duration = (performance.now() - start).toFixed(2);
32
+ console.log(
33
+ `[PIPELINE] Processor ${p.callback.name || "anonymous"} took ${duration}ms`,
34
+ );
35
+ }
36
+ }
37
+ }
38
+
39
+ /**
40
+ * Standard WordPress-style Filters for non-DOM data.
41
+ */
42
+ addFilter(tag, callback, priority = 10) {
43
+ if (!this.#filters.has(tag)) this.#filters.set(tag, []);
44
+ this.#filters.get(tag).push({ callback, priority });
45
+ this.#filters.get(tag).sort((a, b) => a.priority - b.priority);
46
+ }
47
+
48
+ async applyFilters(tag, value, ...args) {
49
+ const hooks = this.#filters.get(tag) || [];
50
+ let result = value;
51
+ for (const h of hooks) {
52
+ result = await h.callback(result, ...args);
53
+ }
54
+ return result;
55
+ }
56
+
57
+ /**
58
+ * Standard WordPress-style Events for side-effects.
59
+ */
60
+ addEvent(tag, callback, priority = 10) {
61
+ if (!this.#events.has(tag)) this.#events.set(tag, []);
62
+ this.#events.get(tag).push({ callback, priority });
63
+ this.#events.get(tag).sort((a, b) => a.priority - b.priority);
64
+ }
65
+
66
+ async emitEvent(tag, ...args) {
67
+ const hooks = this.#events.get(tag) || [];
68
+ for (const h of hooks) {
69
+ await h.callback(...args);
70
+ }
71
+ }
72
+ }