@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,115 @@
|
|
|
1
|
+
import HookRegistry from "./HookRegistry.js";
|
|
2
|
+
import ToolRegistry from "./ToolRegistry.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* createHooks returns a structured, strictly-typed API for registering
|
|
6
|
+
* and emitting hooks, removing the dynamic stringly-typed Proxy magic.
|
|
7
|
+
*/
|
|
8
|
+
export default function createHooks(debug = false) {
|
|
9
|
+
const registry = new HookRegistry(debug);
|
|
10
|
+
const tools = new ToolRegistry();
|
|
11
|
+
|
|
12
|
+
const createEvent = (tag) => ({
|
|
13
|
+
on: (callback, priority) => registry.addEvent(tag, callback, priority),
|
|
14
|
+
emit: (...args) => registry.emitEvent(tag, ...args),
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
const createFilter = (tag) => ({
|
|
18
|
+
addFilter: (callback, priority) =>
|
|
19
|
+
registry.addFilter(tag, callback, priority),
|
|
20
|
+
filter: (value, ...args) => registry.applyFilters(tag, value, ...args),
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
// Core Turn Pipeline
|
|
25
|
+
onTurn: registry.onTurn.bind(registry),
|
|
26
|
+
processTurn: registry.processTurn.bind(registry),
|
|
27
|
+
|
|
28
|
+
// Explicit Hook Schema
|
|
29
|
+
project: {
|
|
30
|
+
init: {
|
|
31
|
+
started: createEvent("project.init.started"),
|
|
32
|
+
completed: createEvent("project.init.completed"),
|
|
33
|
+
},
|
|
34
|
+
files: {
|
|
35
|
+
update: {
|
|
36
|
+
started: createEvent("project.files.update.started"),
|
|
37
|
+
completed: createEvent("project.files.update.completed"),
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
run: {
|
|
42
|
+
started: createEvent("run.started"),
|
|
43
|
+
progress: createEvent("run.progress"),
|
|
44
|
+
state: createEvent("run.state"),
|
|
45
|
+
config: createFilter("run.config"),
|
|
46
|
+
step: {
|
|
47
|
+
completed: createEvent("run.step.completed"),
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
turn: {
|
|
51
|
+
started: createEvent("turn.started"),
|
|
52
|
+
response: createEvent("turn.response"),
|
|
53
|
+
proposing: createEvent("turn.proposing"),
|
|
54
|
+
},
|
|
55
|
+
assembly: {
|
|
56
|
+
system: createFilter("assembly.system"),
|
|
57
|
+
user: createFilter("assembly.user"),
|
|
58
|
+
},
|
|
59
|
+
instructions: {
|
|
60
|
+
toolDocs: createFilter("instructions.toolDocs"),
|
|
61
|
+
},
|
|
62
|
+
ask: {
|
|
63
|
+
started: createEvent("ask.started"),
|
|
64
|
+
completed: createEvent("ask.completed"),
|
|
65
|
+
},
|
|
66
|
+
act: {
|
|
67
|
+
started: createEvent("act.started"),
|
|
68
|
+
completed: createEvent("act.completed"),
|
|
69
|
+
},
|
|
70
|
+
llm: {
|
|
71
|
+
request: {
|
|
72
|
+
started: createEvent("llm.request.started"),
|
|
73
|
+
completed: createEvent("llm.request.completed"),
|
|
74
|
+
},
|
|
75
|
+
messages: createFilter("llm.messages"),
|
|
76
|
+
response: createFilter("llm.response"),
|
|
77
|
+
},
|
|
78
|
+
file: {},
|
|
79
|
+
prompt: {
|
|
80
|
+
tools: createFilter("prompt.tools"),
|
|
81
|
+
},
|
|
82
|
+
entry: {
|
|
83
|
+
created: createEvent("entry.created"),
|
|
84
|
+
changed: createEvent("entry.changed"),
|
|
85
|
+
},
|
|
86
|
+
action: {},
|
|
87
|
+
ui: {
|
|
88
|
+
render: createEvent("ui.render"),
|
|
89
|
+
notify: createEvent("ui.notify"),
|
|
90
|
+
},
|
|
91
|
+
socket: {
|
|
92
|
+
message: {
|
|
93
|
+
raw: createFilter("socket.message.raw"),
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
rpc: {
|
|
97
|
+
started: createEvent("rpc.started"),
|
|
98
|
+
completed: createEvent("rpc.completed"),
|
|
99
|
+
error: createEvent("rpc.error"),
|
|
100
|
+
request: createFilter("rpc.request"),
|
|
101
|
+
response: {
|
|
102
|
+
result: createFilter("rpc.response.result"),
|
|
103
|
+
},
|
|
104
|
+
registry: null, // attached by service.js after RpcRegistry creation
|
|
105
|
+
},
|
|
106
|
+
agent: {},
|
|
107
|
+
tools,
|
|
108
|
+
|
|
109
|
+
// Utility to add raw filters/events directly if needed for tests
|
|
110
|
+
addFilter: registry.addFilter.bind(registry),
|
|
111
|
+
applyFilters: registry.applyFilters.bind(registry),
|
|
112
|
+
addEvent: registry.addEvent.bind(registry),
|
|
113
|
+
emitEvent: registry.emitEvent.bind(registry),
|
|
114
|
+
};
|
|
115
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PluginContext is the plugin-only interface to the rummy system.
|
|
3
|
+
* Available as `rummy.core` on the per-turn RummyContext, and as the
|
|
4
|
+
* direct object passed to plugin constructors at startup.
|
|
5
|
+
*
|
|
6
|
+
* Carries plugin identity, hook registration, and infrastructure access.
|
|
7
|
+
* The unified API (tool verbs, queries) lives on RummyContext.
|
|
8
|
+
* This is the tier boundary: clients can't reach core.
|
|
9
|
+
*/
|
|
10
|
+
export default class PluginContext {
|
|
11
|
+
#name;
|
|
12
|
+
#hooks;
|
|
13
|
+
#db = null;
|
|
14
|
+
#store = null;
|
|
15
|
+
|
|
16
|
+
constructor(name, hooks) {
|
|
17
|
+
this.#name = name;
|
|
18
|
+
this.#hooks = hooks;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
get name() {
|
|
22
|
+
return this.#name;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
get db() {
|
|
26
|
+
return this.#db;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
set db(value) {
|
|
30
|
+
this.#db = value;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
get entries() {
|
|
34
|
+
return this.#store;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
set entries(value) {
|
|
38
|
+
this.#store = value;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
#schemes = [];
|
|
42
|
+
|
|
43
|
+
get hooks() {
|
|
44
|
+
return this.#hooks;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
get schemes() {
|
|
48
|
+
return this.#schemes;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
registerScheme({
|
|
52
|
+
name,
|
|
53
|
+
fidelity = "full",
|
|
54
|
+
modelVisible = 1,
|
|
55
|
+
validStates = ["full", "proposed", "pass", "rejected", "error"],
|
|
56
|
+
category = "result",
|
|
57
|
+
} = {}) {
|
|
58
|
+
this.#schemes.push({
|
|
59
|
+
name: name || this.#name,
|
|
60
|
+
fidelity,
|
|
61
|
+
model_visible: modelVisible,
|
|
62
|
+
valid_states: JSON.stringify(validStates),
|
|
63
|
+
category,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Register a named callback for this plugin.
|
|
69
|
+
* "handler" registers the tool handler.
|
|
70
|
+
* "full"/"summary" register fidelity projections.
|
|
71
|
+
* "docs" sets tool documentation.
|
|
72
|
+
* Everything else resolves to a hook event.
|
|
73
|
+
*/
|
|
74
|
+
on(event, callback, priority = 10) {
|
|
75
|
+
if (event === "handler") {
|
|
76
|
+
this.#hooks.tools.ensureTool(this.#name);
|
|
77
|
+
this.#hooks.tools.onHandle(this.#name, callback, priority);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
if (event === "full" || event === "summary") {
|
|
81
|
+
this.#hooks.tools.ensureTool(this.#name);
|
|
82
|
+
this.#hooks.tools.onView(this.#name, callback, event);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
const hook = this.#resolveEvent(event);
|
|
86
|
+
if (hook) hook.on(callback, priority);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Register a filter callback.
|
|
91
|
+
*/
|
|
92
|
+
filter(name, callback, priority = 10) {
|
|
93
|
+
const hook = this.#resolveFilter(name);
|
|
94
|
+
if (hook) hook.addFilter(callback, priority);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
#resolveEvent(name) {
|
|
98
|
+
const parts = name.split(".");
|
|
99
|
+
let node = this.#hooks;
|
|
100
|
+
for (const part of parts) {
|
|
101
|
+
node = node?.[part];
|
|
102
|
+
}
|
|
103
|
+
if (node?.on) return node;
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
#resolveFilter(name) {
|
|
108
|
+
const parts = name.split(".");
|
|
109
|
+
let node = this.#hooks;
|
|
110
|
+
for (const part of parts) {
|
|
111
|
+
node = node?.[part];
|
|
112
|
+
}
|
|
113
|
+
if (node?.addFilter) return node;
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RummyContext provides a unified, semantic API for plugins to interact with
|
|
3
|
+
* the Turn node tree and core resources like the Database and Project metadata.
|
|
4
|
+
*/
|
|
5
|
+
export default class RummyContext {
|
|
6
|
+
#root;
|
|
7
|
+
#context;
|
|
8
|
+
|
|
9
|
+
constructor(root, context) {
|
|
10
|
+
this.#root = root;
|
|
11
|
+
this.#context = context;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
get hooks() {
|
|
15
|
+
return this.#context.hooks || null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
get db() {
|
|
19
|
+
return this.#context.db;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
get entries() {
|
|
23
|
+
return this.#context.store || null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
get project() {
|
|
27
|
+
return this.#context.project;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
get activeFiles() {
|
|
31
|
+
return this.#context.activeFiles || [];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
get type() {
|
|
35
|
+
return this.#context.type;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
get projectId() {
|
|
39
|
+
return this.#context.projectId;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
get sequence() {
|
|
43
|
+
return this.#context.sequence || 0;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
get runId() {
|
|
47
|
+
return this.#context.runId || null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
get turnId() {
|
|
51
|
+
return this.#context.turnId || null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
get noContext() {
|
|
55
|
+
return this.#context.noContext === true;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
get contextSize() {
|
|
59
|
+
return this.#context.contextSize || null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
get systemPrompt() {
|
|
63
|
+
return this.#context.systemPrompt || "";
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
get loopPrompt() {
|
|
67
|
+
return this.#context.loopPrompt || "";
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
get system() {
|
|
71
|
+
return this.#root.children.find((c) => c.tag === "system");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
get contextEl() {
|
|
75
|
+
return this.#root.children.find((c) => c.tag === "context");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
get user() {
|
|
79
|
+
return this.#root.children.find((c) => c.tag === "user");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
get assistant() {
|
|
83
|
+
return this.#root.children.find((c) => c.tag === "assistant");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// --- Tool methods (same operations the model uses) ---
|
|
87
|
+
|
|
88
|
+
async set({ path, body, state = "full", attributes } = {}) {
|
|
89
|
+
if (!path) {
|
|
90
|
+
const slugify = (await import("../sql/functions/slugify.js")).default;
|
|
91
|
+
const base = slugify(body || "");
|
|
92
|
+
path = `known://${base || Date.now()}`;
|
|
93
|
+
}
|
|
94
|
+
await this.entries.upsert(
|
|
95
|
+
this.runId,
|
|
96
|
+
this.sequence,
|
|
97
|
+
path,
|
|
98
|
+
body || "",
|
|
99
|
+
state,
|
|
100
|
+
attributes ? { attributes } : undefined,
|
|
101
|
+
);
|
|
102
|
+
return path;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async get(path) {
|
|
106
|
+
await this.entries.promoteByPattern(this.runId, path, null, this.sequence);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async store(path) {
|
|
110
|
+
await this.entries.demoteByPattern(this.runId, path, null);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async rm(path) {
|
|
114
|
+
await this.entries.remove(this.runId, path);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async mv(from, to) {
|
|
118
|
+
const body = await this.entries.getBody(this.runId, from);
|
|
119
|
+
if (body === null) return;
|
|
120
|
+
await this.entries.upsert(this.runId, this.sequence, to, body, "full");
|
|
121
|
+
await this.entries.remove(this.runId, from);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async cp(from, to) {
|
|
125
|
+
const body = await this.entries.getBody(this.runId, from);
|
|
126
|
+
if (body === null) return;
|
|
127
|
+
await this.entries.upsert(this.runId, this.sequence, to, body, "full");
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// --- Plugin-only methods (superset) ---
|
|
131
|
+
|
|
132
|
+
async getBody(path) {
|
|
133
|
+
return this.entries.getBody(this.runId, path);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async getAttributes(path) {
|
|
137
|
+
return this.entries.getAttributes(this.runId, path);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async getState(path) {
|
|
141
|
+
const row = await this.entries.getState(this.runId, path);
|
|
142
|
+
return row?.state ?? null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async getEntry(path) {
|
|
146
|
+
const results = await this.entries.getEntriesByPattern(
|
|
147
|
+
this.runId,
|
|
148
|
+
path,
|
|
149
|
+
null,
|
|
150
|
+
);
|
|
151
|
+
return results[0] || null;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async setAttributes(path, attrs) {
|
|
155
|
+
return this.entries.setAttributes(this.runId, path, attrs);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async getEntries(pattern, bodyFilter) {
|
|
159
|
+
return this.entries.getEntriesByPattern(this.runId, pattern, bodyFilter);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async log(message) {
|
|
163
|
+
const path = `content://${Date.now()}`;
|
|
164
|
+
await this.entries.upsert(this.runId, this.sequence, path, message, "info");
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// --- Node tree methods ---
|
|
168
|
+
|
|
169
|
+
tag(name, attrs = {}, children = []) {
|
|
170
|
+
const node = { tag: name, attrs, content: null, children: [] };
|
|
171
|
+
const childArray = Array.isArray(children) ? children : [children];
|
|
172
|
+
for (const child of childArray) {
|
|
173
|
+
if (typeof child === "string") {
|
|
174
|
+
node.content = (node.content || "") + child;
|
|
175
|
+
} else if (child && typeof child === "object") {
|
|
176
|
+
node.children.push(child);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return node;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
export default class ToolRegistry {
|
|
2
|
+
#tools = new Map();
|
|
3
|
+
#handlers = new Map();
|
|
4
|
+
#views = new Map();
|
|
5
|
+
|
|
6
|
+
ensureTool(scheme) {
|
|
7
|
+
if (this.#tools.has(scheme)) return;
|
|
8
|
+
this.#tools.set(scheme, Object.freeze({ modes: new Set(["ask", "act"]) }));
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// Exception: old register() removed. Plugins use core.on("handler")/core.on("full").
|
|
12
|
+
// The only remaining caller pathway is ensureTool + onHandle + onView.
|
|
13
|
+
|
|
14
|
+
get(name) {
|
|
15
|
+
return this.#tools.get(name);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
has(name) {
|
|
19
|
+
return this.#tools.has(name);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
onHandle(scheme, handler, priority = 10) {
|
|
23
|
+
if (!this.#handlers.has(scheme)) this.#handlers.set(scheme, []);
|
|
24
|
+
const list = this.#handlers.get(scheme);
|
|
25
|
+
list.push({ handler, priority });
|
|
26
|
+
list.sort((a, b) => a.priority - b.priority);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
onView(scheme, fn, fidelity = "full") {
|
|
30
|
+
if (!this.#views.has(scheme)) this.#views.set(scheme, new Map());
|
|
31
|
+
this.#views.get(scheme).set(fidelity, fn);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async view(scheme, entry) {
|
|
35
|
+
const fidelityMap = this.#views.get(scheme);
|
|
36
|
+
if (!fidelityMap) {
|
|
37
|
+
throw new Error(
|
|
38
|
+
`No view registered for scheme '${scheme}'. ` +
|
|
39
|
+
`Every tool must define how its entries appear in the model view.`,
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
const fidelity = entry.fidelity || "full";
|
|
43
|
+
const fn = fidelityMap.get(fidelity);
|
|
44
|
+
if (!fn) return "";
|
|
45
|
+
return await fn(entry);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
hasView(scheme) {
|
|
49
|
+
const fidelityMap = this.#views.get(scheme);
|
|
50
|
+
return fidelityMap?.size > 0;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async dispatch(scheme, entry, rummy) {
|
|
54
|
+
const list = this.#handlers.get(scheme);
|
|
55
|
+
if (!list) return;
|
|
56
|
+
for (const { handler } of list) {
|
|
57
|
+
const result = await handler(entry, rummy);
|
|
58
|
+
if (result === false) break;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
get actTools() {
|
|
63
|
+
return new Set(
|
|
64
|
+
[...this.#tools.entries()]
|
|
65
|
+
.filter(([, def]) => def.category === "act")
|
|
66
|
+
.map(([name]) => name),
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
get names() {
|
|
71
|
+
return [...this.#tools.keys()];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
namesForMode(mode) {
|
|
75
|
+
return [...this.#tools.entries()]
|
|
76
|
+
.filter(([, def]) => def.modes.has(mode))
|
|
77
|
+
.map(([name]) => name);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
entries() {
|
|
81
|
+
return this.#tools.entries();
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import msg from "../agent/messages.js";
|
|
2
|
+
import OllamaClient from "./OllamaClient.js";
|
|
3
|
+
import OpenAiClient from "./OpenAiClient.js";
|
|
4
|
+
import OpenRouterClient from "./OpenRouterClient.js";
|
|
5
|
+
import XaiClient from "./XaiClient.js";
|
|
6
|
+
|
|
7
|
+
export default class LlmProvider {
|
|
8
|
+
#db;
|
|
9
|
+
#openRouter;
|
|
10
|
+
#ollama;
|
|
11
|
+
#openAi;
|
|
12
|
+
#xai;
|
|
13
|
+
|
|
14
|
+
constructor(db) {
|
|
15
|
+
this.#db = db;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
#getOpenRouter() {
|
|
19
|
+
this.#openRouter ??= new OpenRouterClient(process.env.OPENROUTER_API_KEY);
|
|
20
|
+
return this.#openRouter;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
#getOllama() {
|
|
24
|
+
this.#ollama ??= new OllamaClient(process.env.OLLAMA_BASE_URL);
|
|
25
|
+
return this.#ollama;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
#getOpenAi() {
|
|
29
|
+
if (!this.#openAi) {
|
|
30
|
+
const baseUrl = process.env.OPENAI_BASE_URL;
|
|
31
|
+
if (!baseUrl) throw new Error(msg("error.openai_base_url_missing"));
|
|
32
|
+
this.#openAi = new OpenAiClient(baseUrl, process.env.OPENAI_API_KEY);
|
|
33
|
+
}
|
|
34
|
+
return this.#openAi;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
#getXai() {
|
|
38
|
+
if (!this.#xai) {
|
|
39
|
+
const baseUrl = process.env.XAI_BASE_URL;
|
|
40
|
+
if (!baseUrl) throw new Error(msg("error.xai_base_url_missing"));
|
|
41
|
+
this.#xai = new XaiClient(baseUrl, process.env.XAI_API_KEY);
|
|
42
|
+
}
|
|
43
|
+
return this.#xai;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async resolve(alias) {
|
|
47
|
+
const row = await this.#db.get_model_by_alias.get({ alias });
|
|
48
|
+
if (row) return row.actual;
|
|
49
|
+
throw new Error(msg("error.model_alias_unknown", { alias }));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async completion(messages, model, options = {}) {
|
|
53
|
+
const resolvedModel = await this.resolve(model);
|
|
54
|
+
|
|
55
|
+
const temperature =
|
|
56
|
+
options.temperature ??
|
|
57
|
+
(process.env.RUMMY_TEMPERATURE !== undefined
|
|
58
|
+
? Number.parseFloat(process.env.RUMMY_TEMPERATURE)
|
|
59
|
+
: undefined);
|
|
60
|
+
const resolvedOptions = { ...options, temperature };
|
|
61
|
+
|
|
62
|
+
if (resolvedModel.startsWith("ollama/")) {
|
|
63
|
+
const localModel = resolvedModel.replace("ollama/", "");
|
|
64
|
+
return this.#getOllama().completion(
|
|
65
|
+
messages,
|
|
66
|
+
localModel,
|
|
67
|
+
resolvedOptions,
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (resolvedModel.startsWith("openai/")) {
|
|
72
|
+
const localModel = resolvedModel.replace("openai/", "");
|
|
73
|
+
return this.#getOpenAi().completion(
|
|
74
|
+
messages,
|
|
75
|
+
localModel,
|
|
76
|
+
resolvedOptions,
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (resolvedModel.startsWith("x.ai/")) {
|
|
81
|
+
const localModel = resolvedModel.replace("x.ai/", "");
|
|
82
|
+
return this.#getXai().completion(messages, localModel, resolvedOptions);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return this.#getOpenRouter().completion(
|
|
86
|
+
messages,
|
|
87
|
+
resolvedModel,
|
|
88
|
+
resolvedOptions,
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async getContextSize(model) {
|
|
93
|
+
const resolvedModel = await this.resolve(model);
|
|
94
|
+
if (resolvedModel.startsWith("ollama/")) {
|
|
95
|
+
const localModel = resolvedModel.replace("ollama/", "");
|
|
96
|
+
return this.#getOllama().getContextSize(localModel);
|
|
97
|
+
}
|
|
98
|
+
if (resolvedModel.startsWith("openai/")) {
|
|
99
|
+
return this.#getOpenAi().getContextSize(resolvedModel);
|
|
100
|
+
}
|
|
101
|
+
if (resolvedModel.startsWith("x.ai/")) {
|
|
102
|
+
const localModel = resolvedModel.replace("x.ai/", "");
|
|
103
|
+
return this.#getXai().getContextSize(localModel);
|
|
104
|
+
}
|
|
105
|
+
return this.#getOpenRouter().getContextSize(resolvedModel);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import msg from "../agent/messages.js";
|
|
2
|
+
|
|
3
|
+
export default class OllamaClient {
|
|
4
|
+
#baseUrl;
|
|
5
|
+
|
|
6
|
+
constructor(baseUrl) {
|
|
7
|
+
this.#baseUrl = baseUrl;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
async completion(messages, model, options = {}) {
|
|
11
|
+
const body = { model, messages, think: true };
|
|
12
|
+
if (options.temperature !== undefined)
|
|
13
|
+
body.temperature = options.temperature;
|
|
14
|
+
|
|
15
|
+
const timeout = Number(process.env.RUMMY_FETCH_TIMEOUT) || 30_000;
|
|
16
|
+
const timeoutSignal = AbortSignal.timeout(timeout);
|
|
17
|
+
const signal = options.signal
|
|
18
|
+
? AbortSignal.any([options.signal, timeoutSignal])
|
|
19
|
+
: timeoutSignal;
|
|
20
|
+
|
|
21
|
+
const response = await fetch(`${this.#baseUrl}/v1/chat/completions`, {
|
|
22
|
+
method: "POST",
|
|
23
|
+
headers: { "Content-Type": "application/json" },
|
|
24
|
+
body: JSON.stringify(body),
|
|
25
|
+
signal,
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
if (!response.ok) {
|
|
29
|
+
const error = await response.text();
|
|
30
|
+
throw new Error(
|
|
31
|
+
msg("error.ollama_api", { status: `${response.status} - ${error}` }),
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const data = await response.json();
|
|
36
|
+
|
|
37
|
+
for (const choice of data.choices || []) {
|
|
38
|
+
const msg = choice.message;
|
|
39
|
+
if (!msg) continue;
|
|
40
|
+
const parts = [msg.reasoning_content, msg.reasoning, msg.thinking].filter(
|
|
41
|
+
Boolean,
|
|
42
|
+
);
|
|
43
|
+
msg.reasoning_content =
|
|
44
|
+
parts.length > 0 ? [...new Set(parts)].join("\n") : null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return data;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async getContextSize(model) {
|
|
51
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
52
|
+
try {
|
|
53
|
+
const response = await fetch(`${this.#baseUrl}/api/show`, {
|
|
54
|
+
method: "POST",
|
|
55
|
+
headers: { "Content-Type": "application/json" },
|
|
56
|
+
body: JSON.stringify({ model }),
|
|
57
|
+
signal: AbortSignal.timeout(
|
|
58
|
+
Number(process.env.RUMMY_FETCH_TIMEOUT) || 30_000,
|
|
59
|
+
),
|
|
60
|
+
});
|
|
61
|
+
if (!response.ok) {
|
|
62
|
+
throw new Error(
|
|
63
|
+
msg("error.ollama_show_failed", {
|
|
64
|
+
status: response.status,
|
|
65
|
+
baseUrl: this.#baseUrl,
|
|
66
|
+
}),
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
const data = await response.json();
|
|
70
|
+
const info = data.model_info || {};
|
|
71
|
+
for (const [key, value] of Object.entries(info)) {
|
|
72
|
+
if (key.endsWith(".context_length")) return value;
|
|
73
|
+
}
|
|
74
|
+
throw new Error(msg("error.ollama_no_context_length", { model }));
|
|
75
|
+
} catch (err) {
|
|
76
|
+
if (err.message.includes("Ollama")) throw err;
|
|
77
|
+
if (attempt < 2) {
|
|
78
|
+
await new Promise((r) => setTimeout(r, (attempt + 1) * 2000));
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
throw new Error(
|
|
82
|
+
msg("error.ollama_unreachable", { baseUrl: this.#baseUrl }),
|
|
83
|
+
{ cause: err },
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|