@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.
- package/.env.example +55 -0
- package/LICENSE +21 -0
- package/PLUGINS.md +302 -0
- package/README.md +41 -0
- package/SPEC.md +524 -0
- package/lang/en.json +34 -0
- package/migrations/001_initial_schema.sql +226 -0
- package/package.json +54 -0
- package/service.js +143 -0
- package/src/agent/AgentLoop.js +553 -0
- package/src/agent/ContextAssembler.js +29 -0
- package/src/agent/KnownStore.js +254 -0
- package/src/agent/ProjectAgent.js +101 -0
- package/src/agent/ResponseHealer.js +134 -0
- package/src/agent/TurnExecutor.js +457 -0
- package/src/agent/XmlParser.js +247 -0
- package/src/agent/known_checks.sql +42 -0
- package/src/agent/known_queries.sql +80 -0
- package/src/agent/known_store.sql +161 -0
- package/src/agent/messages.js +17 -0
- package/src/agent/prompt_queue.sql +39 -0
- package/src/agent/runs.sql +114 -0
- package/src/agent/schemes.sql +3 -0
- package/src/agent/sessions.sql +51 -0
- package/src/agent/tokens.js +28 -0
- package/src/agent/turns.sql +36 -0
- package/src/hooks/HookRegistry.js +72 -0
- package/src/hooks/Hooks.js +115 -0
- package/src/hooks/PluginContext.js +116 -0
- package/src/hooks/RummyContext.js +181 -0
- package/src/hooks/ToolRegistry.js +83 -0
- package/src/llm/LlmProvider.js +107 -0
- package/src/llm/OllamaClient.js +88 -0
- package/src/llm/OpenAiClient.js +80 -0
- package/src/llm/OpenRouterClient.js +78 -0
- package/src/llm/XaiClient.js +113 -0
- package/src/plugins/ask_user/README.md +18 -0
- package/src/plugins/ask_user/ask_user.js +48 -0
- package/src/plugins/ask_user/docs.md +2 -0
- package/src/plugins/cp/README.md +18 -0
- package/src/plugins/cp/cp.js +55 -0
- package/src/plugins/cp/docs.md +2 -0
- package/src/plugins/current/README.md +14 -0
- package/src/plugins/current/current.js +48 -0
- package/src/plugins/engine/README.md +12 -0
- package/src/plugins/engine/engine.sql +18 -0
- package/src/plugins/engine/turn_context.sql +51 -0
- package/src/plugins/env/README.md +14 -0
- package/src/plugins/env/docs.md +2 -0
- package/src/plugins/env/env.js +32 -0
- package/src/plugins/file/README.md +25 -0
- package/src/plugins/file/file.js +85 -0
- package/src/plugins/get/README.md +19 -0
- package/src/plugins/get/docs.md +6 -0
- package/src/plugins/get/get.js +53 -0
- package/src/plugins/hedberg/README.md +72 -0
- package/src/plugins/hedberg/docs.md +9 -0
- package/src/plugins/hedberg/edits.js +65 -0
- package/src/plugins/hedberg/hedberg.js +89 -0
- package/src/plugins/hedberg/matcher.js +181 -0
- package/src/plugins/hedberg/normalize.js +41 -0
- package/src/plugins/hedberg/patterns.js +452 -0
- package/src/plugins/hedberg/sed.js +48 -0
- package/src/plugins/helpers.js +22 -0
- package/src/plugins/index.js +180 -0
- package/src/plugins/instructions/README.md +11 -0
- package/src/plugins/instructions/instructions.js +37 -0
- package/src/plugins/instructions/preamble.md +12 -0
- package/src/plugins/known/README.md +18 -0
- package/src/plugins/known/docs.md +3 -0
- package/src/plugins/known/known.js +57 -0
- package/src/plugins/mv/README.md +18 -0
- package/src/plugins/mv/docs.md +2 -0
- package/src/plugins/mv/mv.js +56 -0
- package/src/plugins/previous/README.md +15 -0
- package/src/plugins/previous/previous.js +50 -0
- package/src/plugins/progress/README.md +17 -0
- package/src/plugins/progress/progress.js +44 -0
- package/src/plugins/prompt/README.md +16 -0
- package/src/plugins/prompt/prompt.js +45 -0
- package/src/plugins/rm/README.md +18 -0
- package/src/plugins/rm/docs.md +4 -0
- package/src/plugins/rm/rm.js +51 -0
- package/src/plugins/rpc/README.md +45 -0
- package/src/plugins/rpc/rpc.js +587 -0
- package/src/plugins/set/README.md +32 -0
- package/src/plugins/set/docs.md +4 -0
- package/src/plugins/set/set.js +268 -0
- package/src/plugins/sh/README.md +18 -0
- package/src/plugins/sh/docs.md +2 -0
- package/src/plugins/sh/sh.js +32 -0
- package/src/plugins/skills/README.md +25 -0
- package/src/plugins/skills/skills.js +175 -0
- package/src/plugins/store/README.md +20 -0
- package/src/plugins/store/docs.md +5 -0
- package/src/plugins/store/store.js +52 -0
- package/src/plugins/summarize/README.md +18 -0
- package/src/plugins/summarize/docs.md +4 -0
- package/src/plugins/summarize/summarize.js +24 -0
- package/src/plugins/telemetry/README.md +19 -0
- package/src/plugins/telemetry/rpc_log.sql +28 -0
- package/src/plugins/telemetry/telemetry.js +186 -0
- package/src/plugins/unknown/README.md +23 -0
- package/src/plugins/unknown/docs.md +5 -0
- package/src/plugins/unknown/unknown.js +31 -0
- package/src/plugins/update/README.md +18 -0
- package/src/plugins/update/docs.md +4 -0
- package/src/plugins/update/update.js +24 -0
- package/src/server/ClientConnection.js +228 -0
- package/src/server/RpcRegistry.js +52 -0
- package/src/server/SocketServer.js +43 -0
- package/src/sql/file_constraints.sql +15 -0
- package/src/sql/functions/countTokens.js +7 -0
- package/src/sql/functions/hedmatch.js +8 -0
- package/src/sql/functions/hedreplace.js +8 -0
- package/src/sql/functions/hedsearch.js +8 -0
- package/src/sql/functions/schemeOf.js +7 -0
- package/src/sql/functions/slugify.js +6 -0
- package/src/sql/v_model_context.sql +101 -0
- 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,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,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,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.
|