@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,80 @@
1
+ import msg from "../agent/messages.js";
2
+
3
+ export default class OpenAiClient {
4
+ #baseUrl;
5
+ #apiKey;
6
+
7
+ constructor(baseUrl, apiKey) {
8
+ this.#baseUrl = String(baseUrl || "").replace(/\/v1\/?$/, "");
9
+ this.#apiKey = apiKey || "";
10
+ }
11
+
12
+ async completion(messages, model, options = {}) {
13
+ const body = { model, messages, think: true };
14
+ if (options.temperature !== undefined)
15
+ body.temperature = options.temperature;
16
+
17
+ const timeout = Number(process.env.RUMMY_FETCH_TIMEOUT) || 30_000;
18
+ const timeoutSignal = AbortSignal.timeout(timeout);
19
+ const signal = options.signal
20
+ ? AbortSignal.any([options.signal, timeoutSignal])
21
+ : timeoutSignal;
22
+
23
+ const headers = { "Content-Type": "application/json" };
24
+ if (this.#apiKey) headers.Authorization = `Bearer ${this.#apiKey}`;
25
+
26
+ const response = await fetch(`${this.#baseUrl}/v1/chat/completions`, {
27
+ method: "POST",
28
+ headers,
29
+ body: JSON.stringify(body),
30
+ signal,
31
+ });
32
+
33
+ if (!response.ok) {
34
+ const error = await response.text();
35
+ throw new Error(
36
+ msg("error.openai_api", { status: `${response.status} - ${error}` }),
37
+ );
38
+ }
39
+
40
+ const data = await response.json();
41
+
42
+ for (const choice of data.choices || []) {
43
+ const msg = choice.message;
44
+ if (!msg) continue;
45
+
46
+ // Normalize reasoning
47
+ const parts = [msg.reasoning_content, msg.reasoning, msg.thinking].filter(
48
+ Boolean,
49
+ );
50
+ msg.reasoning_content =
51
+ parts.length > 0 ? [...new Set(parts)].join("\n") : null;
52
+ }
53
+
54
+ return data;
55
+ }
56
+
57
+ async getContextSize(_model) {
58
+ const timeout = Number(process.env.RUMMY_FETCH_TIMEOUT) || 30_000;
59
+ const headers = { "Content-Type": "application/json" };
60
+ if (this.#apiKey) headers.Authorization = `Bearer ${this.#apiKey}`;
61
+
62
+ const response = await fetch(`${this.#baseUrl}/v1/models`, {
63
+ headers,
64
+ signal: AbortSignal.timeout(timeout),
65
+ });
66
+ if (!response.ok) {
67
+ throw new Error(
68
+ msg("error.openai_models_failed", {
69
+ status: response.status,
70
+ baseUrl: this.#baseUrl,
71
+ }),
72
+ );
73
+ }
74
+ const data = await response.json();
75
+ const model = data.data?.[0];
76
+ const ctx = model?.meta?.n_ctx_train || model?.context_length;
77
+ if (!ctx) throw new Error(msg("error.openai_no_context_length"));
78
+ return ctx;
79
+ }
80
+ }
@@ -0,0 +1,78 @@
1
+ import msg from "../agent/messages.js";
2
+
3
+ const DEFAULT_CONTEXT_SIZE = 131072;
4
+
5
+ export default class OpenRouterClient {
6
+ #apiKey;
7
+ #baseUrl;
8
+
9
+ constructor(apiKey) {
10
+ this.#apiKey = apiKey;
11
+ this.#baseUrl = process.env.OPENROUTER_BASE_URL;
12
+ }
13
+
14
+ async completion(messages, model, options = {}) {
15
+ if (!this.#apiKey) throw new Error(msg("error.openrouter_api_key_missing"));
16
+ return this.#fetch(messages, model, options);
17
+ }
18
+
19
+ async #fetch(messages, model, options) {
20
+ const body = { model, messages, include_reasoning: true };
21
+ if (options.temperature !== undefined)
22
+ body.temperature = options.temperature;
23
+
24
+ const timeout = Number(process.env.RUMMY_FETCH_TIMEOUT) || 30_000;
25
+ const timeoutSignal = AbortSignal.timeout(timeout);
26
+ const signal = options.signal
27
+ ? AbortSignal.any([options.signal, timeoutSignal])
28
+ : timeoutSignal;
29
+
30
+ const response = await fetch(`${this.#baseUrl}/chat/completions`, {
31
+ method: "POST",
32
+ headers: {
33
+ Authorization: `Bearer ${this.#apiKey}`,
34
+ "Content-Type": "application/json",
35
+ "HTTP-Referer": process.env.RUMMY_HTTP_REFERER,
36
+ "X-Title": process.env.RUMMY_X_TITLE,
37
+ },
38
+ body: JSON.stringify(body),
39
+ signal,
40
+ });
41
+
42
+ if (!response.ok) {
43
+ const error = await response.text();
44
+ if (response.status === 401 || response.status === 403) {
45
+ throw new Error(
46
+ msg("error.openrouter_auth", {
47
+ status: `${response.status} - ${error}`,
48
+ }),
49
+ );
50
+ }
51
+ throw new Error(
52
+ msg("error.openrouter_api", {
53
+ status: `${response.status} - ${error}`,
54
+ }),
55
+ );
56
+ }
57
+ const data = await response.json();
58
+
59
+ for (const choice of data.choices || []) {
60
+ const cm = choice.message;
61
+ if (!cm) continue;
62
+ const parts = [
63
+ cm.reasoning_content,
64
+ cm.reasoning,
65
+ cm.thinking,
66
+ ...(cm.reasoning_details || []).map((d) => d.text),
67
+ ].filter(Boolean);
68
+ cm.reasoning_content =
69
+ parts.length > 0 ? [...new Set(parts)].join("\n") : null;
70
+ }
71
+
72
+ return data;
73
+ }
74
+
75
+ async getContextSize(_model) {
76
+ return Number(process.env.RUMMY_CONTEXT_SIZE) || DEFAULT_CONTEXT_SIZE;
77
+ }
78
+ }
@@ -0,0 +1,113 @@
1
+ import msg from "../agent/messages.js";
2
+
3
+ export default class XaiClient {
4
+ #baseUrl;
5
+ #apiKey;
6
+
7
+ constructor(baseUrl, apiKey) {
8
+ this.#baseUrl = baseUrl;
9
+ this.#apiKey = apiKey;
10
+ }
11
+
12
+ async completion(messages, model, options = {}) {
13
+ if (!this.#apiKey) throw new Error(msg("error.xai_api_key_missing"));
14
+
15
+ const body = { model, input: messages };
16
+ if (options.temperature !== undefined)
17
+ body.temperature = options.temperature;
18
+
19
+ const timeout = Number(process.env.RUMMY_FETCH_TIMEOUT) || 30_000;
20
+ const timeoutSignal = AbortSignal.timeout(timeout);
21
+ const signal = options.signal
22
+ ? AbortSignal.any([options.signal, timeoutSignal])
23
+ : timeoutSignal;
24
+
25
+ const response = await fetch(this.#baseUrl, {
26
+ method: "POST",
27
+ headers: {
28
+ Authorization: `Bearer ${this.#apiKey}`,
29
+ "Content-Type": "application/json",
30
+ },
31
+ body: JSON.stringify(body),
32
+ signal,
33
+ });
34
+
35
+ if (!response.ok) {
36
+ const error = await response.text();
37
+ if (response.status === 401 || response.status === 403) {
38
+ throw new Error(
39
+ msg("error.xai_auth", {
40
+ status: `${response.status} - ${error}`,
41
+ }),
42
+ );
43
+ }
44
+ throw new Error(
45
+ msg("error.xai_api", {
46
+ status: `${response.status} - ${error}`,
47
+ }),
48
+ );
49
+ }
50
+
51
+ const data = await response.json();
52
+ return this.#normalize(data);
53
+ }
54
+
55
+ #normalize(data) {
56
+ const output = data.output || [];
57
+
58
+ let content = "";
59
+ let reasoningContent = null;
60
+
61
+ for (const item of output) {
62
+ if (item.type === "reasoning") {
63
+ const text = this.#extractText(item.content);
64
+ if (text)
65
+ reasoningContent = reasoningContent
66
+ ? `${reasoningContent}\n${text}`
67
+ : text;
68
+ }
69
+ if (item.type === "message") {
70
+ const text = this.#extractText(item.content);
71
+ if (text) content = content ? `${content}\n${text}` : text;
72
+ }
73
+ }
74
+
75
+ const usage = data.usage || {};
76
+ const inputTokens = usage.input_tokens || 0;
77
+ const outputTokens = usage.output_tokens || 0;
78
+ return {
79
+ choices: [
80
+ {
81
+ message: {
82
+ role: "assistant",
83
+ content,
84
+ reasoning_content: reasoningContent,
85
+ },
86
+ },
87
+ ],
88
+ usage: {
89
+ prompt_tokens: inputTokens,
90
+ cached_tokens: usage.input_tokens_details?.cached_tokens || 0,
91
+ completion_tokens: outputTokens,
92
+ reasoning_tokens: usage.output_tokens_details?.reasoning_tokens || 0,
93
+ total_tokens: inputTokens + outputTokens,
94
+ cost: (usage.cost_in_usd_ticks || 0) / 10_000_000_000,
95
+ },
96
+ };
97
+ }
98
+
99
+ #extractText(content) {
100
+ if (typeof content === "string") return content;
101
+ if (!Array.isArray(content)) return null;
102
+ return (
103
+ content
104
+ .filter((c) => c.type === "text" || c.type === "output_text")
105
+ .map((c) => c.text)
106
+ .join("\n") || null
107
+ );
108
+ }
109
+
110
+ async getContextSize(_model) {
111
+ return Number(process.env.RUMMY_CONTEXT_SIZE) || 131072;
112
+ }
113
+ }
@@ -0,0 +1,18 @@
1
+ # ask_user
2
+
3
+ Presents a question to the user with optional multiple-choice answers.
4
+
5
+ ## Registration
6
+
7
+ - **Tool**: `ask_user`
8
+ - **Modes**: ask, act
9
+ - **Category**: act
10
+ - **Handler**: Parses options (semicolon or comma delimited) and upserts a `proposed` entry awaiting user response.
11
+
12
+ ## Projection
13
+
14
+ Shows the question and answer attributes.
15
+
16
+ ## Behavior
17
+
18
+ Options are split by semicolons first, falling back to commas. The entry stays in `proposed` state until resolved by the client.
@@ -0,0 +1,48 @@
1
+ import { readFileSync } from "node:fs";
2
+
3
+ export default class AskUser {
4
+ #core;
5
+
6
+ constructor(core) {
7
+ this.#core = core;
8
+ core.registerScheme();
9
+ core.on("handler", this.handler.bind(this));
10
+ core.on("full", this.full.bind(this));
11
+ core.on("summary", this.summary.bind(this));
12
+ const docs = readFileSync(new URL("./docs.md", import.meta.url), "utf8");
13
+ core.filter("instructions.toolDocs", async (content) =>
14
+ content ? `${content}\n\n${docs}` : docs,
15
+ );
16
+ }
17
+
18
+ async handler(entry, rummy) {
19
+ const { entries: store, sequence: turn, runId } = rummy;
20
+ const { question, options: rawOptions } = entry.attributes;
21
+
22
+ const optionText = rawOptions || entry.body || "";
23
+ const delimiter = optionText.includes(";") ? ";" : ",";
24
+ const options = optionText
25
+ ? optionText
26
+ .split(delimiter)
27
+ .map((o) => o.trim())
28
+ .filter(Boolean)
29
+ : [];
30
+
31
+ await store.upsert(runId, turn, entry.resultPath, entry.body, "proposed", {
32
+ attributes: { question, options },
33
+ });
34
+ }
35
+
36
+ full(entry) {
37
+ const { question, answer } = entry.attributes;
38
+ const lines = ["# ask_user"];
39
+ if (question) lines.push(`# Question: ${question}`);
40
+ if (answer) lines.push(`# Answer: ${answer}`);
41
+ return lines.join("\n");
42
+ }
43
+
44
+ summary(entry) {
45
+ const { question, answer } = entry.attributes;
46
+ return answer ? `${question} → ${answer}` : question || "";
47
+ }
48
+ }
@@ -0,0 +1,2 @@
1
+ ## <ask_user question="[Question?]">[option1; option2; ...]</ask_user>
2
+ Example: <ask_user question="Which test framework?">Mocha; Jest; Node Native</ask_user>
@@ -0,0 +1,18 @@
1
+ # cp
2
+
3
+ Copies an entry from one path to another within the K/V store.
4
+
5
+ ## Registration
6
+
7
+ - **Tool**: `cp`
8
+ - **Modes**: ask, act
9
+ - **Category**: act
10
+ - **Handler**: Reads source body, writes to destination. K/V destinations resolve immediately (`pass`); file destinations produce a `proposed` entry.
11
+
12
+ ## Projection
13
+
14
+ Shows `cp {from} {to}`.
15
+
16
+ ## Behavior
17
+
18
+ Warns if the destination already exists and will be overwritten. Uses `KnownStore.scheme()` to determine whether the destination is a K/V path or a file path.
@@ -0,0 +1,55 @@
1
+ import { readFileSync } from "node:fs";
2
+ import KnownStore from "../../agent/KnownStore.js";
3
+
4
+ export default class Cp {
5
+ #core;
6
+
7
+ constructor(core) {
8
+ this.#core = core;
9
+ core.registerScheme({
10
+ validStates: ["full", "proposed", "pass", "rejected", "error", "pattern"],
11
+ });
12
+ core.on("handler", this.handler.bind(this));
13
+ core.on("full", this.full.bind(this));
14
+ core.on("summary", this.summary.bind(this));
15
+ const docs = readFileSync(new URL("./docs.md", import.meta.url), "utf8");
16
+ core.filter("instructions.toolDocs", async (content) =>
17
+ content ? `${content}\n\n${docs}` : docs,
18
+ );
19
+ }
20
+
21
+ async handler(entry, rummy) {
22
+ const { entries: store, sequence: turn, runId } = rummy;
23
+ const { path, to } = entry.attributes;
24
+
25
+ const source = await store.getBody(runId, path);
26
+ if (source === null) return;
27
+
28
+ const destScheme = KnownStore.scheme(to);
29
+ const existing = await store.getBody(runId, to);
30
+ const warning =
31
+ existing !== null && destScheme !== null
32
+ ? `Overwrote existing entry at ${to}`
33
+ : null;
34
+
35
+ const body = `${path} ${to}`;
36
+ if (destScheme === null) {
37
+ await store.upsert(runId, turn, entry.resultPath, body, "proposed", {
38
+ attributes: { from: path, to, isMove: false, warning },
39
+ });
40
+ } else {
41
+ await store.upsert(runId, turn, to, source, "full");
42
+ await store.upsert(runId, turn, entry.resultPath, body, "pass", {
43
+ attributes: { from: path, to, isMove: false, warning },
44
+ });
45
+ }
46
+ }
47
+
48
+ full(entry) {
49
+ return `# cp ${entry.attributes.from || ""} ${entry.attributes.to || ""}`;
50
+ }
51
+
52
+ summary(entry) {
53
+ return this.full(entry);
54
+ }
55
+ }
@@ -0,0 +1,2 @@
1
+ ## <cp path="[path/to/origin"]>[path/to/destination]</cp> - Copy a file or entry
2
+ Example: <cp path="docs/example.txt">docs/example_copy.txt</cp>
@@ -0,0 +1,14 @@
1
+ # current
2
+
3
+ Renders the `<current>` section of the user message — the active loop's
4
+ model responses, tool results, and agent warnings.
5
+
6
+ ## Registration
7
+
8
+ - **Filter**: `assembly.user` at priority 100
9
+
10
+ ## Behavior
11
+
12
+ Filters turn_context rows where `category` is `result` or `structural`
13
+ and `source_turn >= loopStartTurn`. Renders each entry chronologically
14
+ with status symbols. Empty on the first turn of a loop.
@@ -0,0 +1,48 @@
1
+ export default class Current {
2
+ #core;
3
+
4
+ constructor(core) {
5
+ this.#core = core;
6
+ core.filter("assembly.user", this.assembleCurrent.bind(this), 100);
7
+ }
8
+
9
+ async assembleCurrent(content, ctx) {
10
+ const entries = ctx.rows.filter(
11
+ (r) =>
12
+ (r.category === "result" || r.category === "structural") &&
13
+ r.source_turn >= ctx.loopStartTurn,
14
+ );
15
+ if (entries.length === 0) return content;
16
+
17
+ const lines = await Promise.all(
18
+ entries.map((e) => renderToolTag(e, "full", this.#core)),
19
+ );
20
+ return `${content}<current>\n${lines.join("\n")}\n</current>\n`;
21
+ }
22
+ }
23
+
24
+ async function renderToolTag(entry, fidelity, core) {
25
+ const attrs =
26
+ typeof entry.attributes === "string"
27
+ ? JSON.parse(entry.attributes)
28
+ : entry.attributes;
29
+
30
+ const path = `${entry.scheme}://${attrs?.path || attrs?.file || attrs?.command || ""}`;
31
+ const status = entry.state ? ` status="${entry.state}"` : "";
32
+
33
+ let body;
34
+ try {
35
+ body = await core.hooks.tools.view(entry.scheme, {
36
+ ...entry,
37
+ fidelity,
38
+ attributes: attrs,
39
+ });
40
+ } catch {
41
+ body = entry.body;
42
+ }
43
+
44
+ if (body) {
45
+ return `<tool path="${path}"${status}>${body}</tool>`;
46
+ }
47
+ return `<tool path="${path}"${status}/>`;
48
+ }
@@ -0,0 +1,12 @@
1
+ # engine
2
+
3
+ SQL infrastructure for context assembly and turn management. No JS plugin.
4
+
5
+ ## Files
6
+
7
+ - **engine.sql** — Queries for retrieving promoted entries by scheme tier, model visibility, and state.
8
+ - **turn_context.sql** — Queries for clearing and reading the `turn_context` / `v_model_context` view, which produces the ordered context sent to the model.
9
+
10
+ ## Behavior
11
+
12
+ These SQL files are loaded by the database layer and used by the agent loop to assemble per-turn model context. They are not a plugin in the `register()` sense.
@@ -0,0 +1,18 @@
1
+ -- PREP: get_promoted_entries
2
+ SELECT ke.path, ke.scheme, ke.state, ke.turn, ke.tokens, ke.refs
3
+ FROM known_entries AS ke
4
+ JOIN schemes AS s ON s.name = COALESCE(ke.scheme, 'file')
5
+ WHERE
6
+ ke.run_id = :run_id
7
+ AND ke.state IN ('full', 'summary')
8
+ AND s.model_visible = 1
9
+ ORDER BY ke.turn, ke.refs, ke.tokens DESC;
10
+
11
+ -- PREP: get_promoted_token_total
12
+ SELECT COALESCE(SUM(ke.tokens), 0) AS total
13
+ FROM known_entries AS ke
14
+ JOIN schemes AS s ON s.name = COALESCE(ke.scheme, 'file')
15
+ WHERE
16
+ ke.run_id = :run_id
17
+ AND ke.state IN ('full', 'summary')
18
+ AND s.model_visible = 1;
@@ -0,0 +1,51 @@
1
+ -- PREP: clear_turn_context
2
+ DELETE FROM turn_context
3
+ WHERE run_id = :run_id AND turn = :turn;
4
+
5
+ -- PREP: get_model_context
6
+ SELECT
7
+ ordinal, path, scheme, fidelity, state, body, tokens, attributes, category, turn
8
+ FROM v_model_context
9
+ WHERE run_id = :run_id
10
+ ORDER BY ordinal;
11
+
12
+ -- PREP: insert_turn_context
13
+ INSERT INTO turn_context (
14
+ run_id, turn, ordinal, path, fidelity, state, body, tokens, attributes, category, source_turn
15
+ )
16
+ VALUES (
17
+ :run_id, :turn, :ordinal, :path, :fidelity, :state, :body, :tokens
18
+ , COALESCE(:attributes, '{}'), :category, :source_turn
19
+ );
20
+
21
+ -- PREP: get_turn_context
22
+ SELECT ordinal, path, scheme, fidelity, state, body, tokens, attributes, category, source_turn
23
+ FROM turn_context
24
+ WHERE run_id = :run_id AND turn = :turn
25
+ ORDER BY ordinal;
26
+
27
+ -- PREP: get_turn_budget
28
+ SELECT COALESCE(SUM(tokens), 0) AS total
29
+ FROM turn_context
30
+ WHERE run_id = :run_id AND turn = :turn;
31
+
32
+ -- PREP: get_turn_distribution
33
+ SELECT
34
+ CASE category
35
+ WHEN 'file' THEN 'files'
36
+ WHEN 'file_symbols' THEN 'files'
37
+ WHEN 'file_index' THEN 'keys'
38
+ WHEN 'known' THEN 'known'
39
+ WHEN 'known_index' THEN 'keys'
40
+ WHEN 'unknown' THEN 'history'
41
+ WHEN 'result' THEN 'history'
42
+ WHEN 'prompt' THEN 'system'
43
+ WHEN 'system' THEN 'system'
44
+ ELSE 'system'
45
+ END AS bucket,
46
+ COALESCE(SUM(tokens), 0) AS tokens,
47
+ COUNT(*) AS entries
48
+ FROM turn_context
49
+ WHERE run_id = :run_id AND turn = :turn
50
+ GROUP BY 1
51
+ ORDER BY 1;
@@ -0,0 +1,14 @@
1
+ # env
2
+
3
+ Stores environment/context information as a pass-through entry.
4
+
5
+ ## Registration
6
+
7
+ - **Tool**: `env`
8
+ - **Modes**: ask, act
9
+ - **Category**: ask
10
+ - **Handler**: Upserts the entry body as `pass` state with original attributes preserved.
11
+
12
+ ## Projection
13
+
14
+ Shows `env {command}` followed by the entry body.
@@ -0,0 +1,2 @@
1
+ ## <env>[command]</env> - Run an exploratory shell command
2
+ Example: <env>npm --version</env>
@@ -0,0 +1,32 @@
1
+ import { readFileSync } from "node:fs";
2
+
3
+ export default class Env {
4
+ #core;
5
+
6
+ constructor(core) {
7
+ this.#core = core;
8
+ core.registerScheme();
9
+ core.on("handler", this.handler.bind(this));
10
+ core.on("full", this.full.bind(this));
11
+ core.on("summary", this.summary.bind(this));
12
+ const docs = readFileSync(new URL("./docs.md", import.meta.url), "utf8");
13
+ core.filter("instructions.toolDocs", async (content) =>
14
+ content ? `${content}\n\n${docs}` : docs,
15
+ );
16
+ }
17
+
18
+ async handler(entry, rummy) {
19
+ const { entries: store, sequence: turn, runId } = rummy;
20
+ await store.upsert(runId, turn, entry.resultPath, entry.body, "pass", {
21
+ attributes: entry.attributes,
22
+ });
23
+ }
24
+
25
+ full(entry) {
26
+ return `# env ${entry.attributes.command || ""}\n${entry.body}`;
27
+ }
28
+
29
+ summary(entry) {
30
+ return entry.attributes.command || "";
31
+ }
32
+ }
@@ -0,0 +1,25 @@
1
+ # file
2
+
3
+ Owns file-related projections and file constraint management.
4
+
5
+ ## Files
6
+
7
+ - **file.js** — Plugin registration, projection hooks, and constraint CRUD (activate, ignore, drop).
8
+ - **FileScanner.js** — Scans project directories for file entries.
9
+ - **GitProvider.js** — Git integration for file discovery and status.
10
+ - **ProjectContext.js** — Builds project-level context from scanned files.
11
+ - **FsProvider.js** — Filesystem abstraction for file reading/writing.
12
+
13
+ ## Registration
14
+
15
+ - **Projections**: Registers `onProject` handlers for schemes: `file`, `known`, `skill`, `ask`, `act`, `progress`. All project the entry body directly.
16
+ - **No tool handler** — file operations are dispatched through `set`, `get`, `rm`, etc.
17
+
18
+ ## File Constraints
19
+
20
+ Static methods `activate`, `ignore`, and `drop` manage per-project file constraints in the database. Constraints control file visibility across all runs:
21
+
22
+ - `active` / `readonly` — always promoted into context.
23
+ - `ignore` — excluded from scans; demotes existing entries.
24
+
25
+ Paths are normalized to project-relative when absolute.