@possumtech/rummy 0.4.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +21 -4
- package/PLUGINS.md +389 -194
- package/README.md +25 -8
- package/SPEC.md +850 -373
- package/bin/demo.js +166 -0
- package/bin/rummy.js +9 -3
- package/biome/no-fallbacks.grit +50 -0
- package/lang/en.json +2 -2
- package/migrations/001_initial_schema.sql +88 -37
- package/package.json +6 -4
- package/service.js +50 -9
- package/src/agent/AgentLoop.js +460 -331
- package/src/agent/ContextAssembler.js +4 -2
- package/src/agent/Entries.js +655 -0
- package/src/agent/ProjectAgent.js +30 -18
- package/src/agent/TurnExecutor.js +232 -379
- package/src/agent/XmlParser.js +242 -67
- package/src/agent/budget.js +56 -0
- package/src/agent/errors.js +22 -0
- package/src/agent/httpStatus.js +39 -0
- package/src/agent/known_checks.sql +8 -4
- package/src/agent/known_queries.sql +9 -13
- package/src/agent/known_store.sql +275 -118
- package/src/agent/materializeContext.js +102 -0
- package/src/agent/runs.sql +10 -7
- package/src/agent/schemes.sql +14 -3
- package/src/agent/turns.sql +9 -9
- package/src/hooks/HookRegistry.js +6 -5
- package/src/hooks/Hooks.js +44 -3
- package/src/hooks/PluginContext.js +35 -21
- package/src/{server → hooks}/RpcRegistry.js +2 -1
- package/src/hooks/RummyContext.js +140 -37
- package/src/hooks/ToolRegistry.js +36 -35
- package/src/llm/LlmProvider.js +64 -90
- package/src/llm/errors.js +21 -0
- package/src/plugins/ask_user/README.md +1 -1
- package/src/plugins/ask_user/ask_user.js +37 -12
- package/src/plugins/ask_user/ask_userDoc.js +2 -23
- package/src/plugins/ask_user/ask_userDoc.md +10 -0
- package/src/plugins/budget/README.md +27 -23
- package/src/plugins/budget/budget.js +261 -69
- package/src/plugins/cp/README.md +2 -2
- package/src/plugins/cp/cp.js +31 -13
- package/src/plugins/cp/cpDoc.js +2 -23
- package/src/plugins/cp/cpDoc.md +7 -0
- package/src/plugins/engine/README.md +2 -2
- package/src/plugins/engine/engine.sql +4 -4
- package/src/plugins/engine/turn_context.sql +10 -10
- package/src/plugins/env/README.md +20 -5
- package/src/plugins/env/env.js +47 -8
- package/src/plugins/env/envDoc.js +2 -23
- package/src/plugins/env/envDoc.md +13 -0
- package/src/plugins/error/README.md +16 -0
- package/src/plugins/error/error.js +151 -0
- package/src/plugins/file/README.md +6 -6
- package/src/plugins/file/file.js +15 -7
- package/src/plugins/get/README.md +1 -1
- package/src/plugins/get/get.js +125 -49
- package/src/plugins/get/getDoc.js +2 -43
- package/src/plugins/get/getDoc.md +36 -0
- package/src/plugins/hedberg/README.md +1 -2
- package/src/plugins/hedberg/hedberg.js +8 -4
- package/src/plugins/hedberg/matcher.js +16 -17
- package/src/plugins/hedberg/normalize.js +0 -48
- package/src/plugins/helpers.js +43 -3
- package/src/plugins/index.js +146 -123
- package/src/plugins/instructions/README.md +35 -9
- package/src/plugins/instructions/instructions.js +126 -12
- package/src/plugins/instructions/instructions.md +25 -0
- package/src/plugins/instructions/instructions_104.md +7 -0
- package/src/plugins/instructions/instructions_105.md +46 -0
- package/src/plugins/instructions/instructions_106.md +0 -0
- package/src/plugins/instructions/instructions_107.md +0 -0
- package/src/plugins/instructions/instructions_108.md +8 -0
- package/src/plugins/instructions/protocol.js +12 -0
- package/src/plugins/known/README.md +2 -2
- package/src/plugins/known/known.js +77 -45
- package/src/plugins/known/knownDoc.js +2 -29
- package/src/plugins/known/knownDoc.md +8 -0
- package/src/plugins/log/README.md +48 -0
- package/src/plugins/log/log.js +109 -0
- package/src/plugins/mv/README.md +2 -2
- package/src/plugins/mv/mv.js +57 -24
- package/src/plugins/mv/mvDoc.js +2 -29
- package/src/plugins/mv/mvDoc.md +10 -0
- package/src/plugins/ollama/README.md +15 -0
- package/src/{llm/OllamaClient.js → plugins/ollama/ollama.js} +40 -18
- package/src/plugins/openai/README.md +17 -0
- package/src/plugins/openai/openai.js +120 -0
- package/src/plugins/openrouter/README.md +27 -0
- package/src/plugins/openrouter/openrouter.js +121 -0
- package/src/plugins/persona/README.md +20 -0
- package/src/plugins/persona/persona.js +9 -16
- package/src/plugins/policy/README.md +21 -0
- package/src/plugins/policy/policy.js +29 -14
- package/src/plugins/prompt/README.md +1 -1
- package/src/plugins/prompt/prompt.js +63 -18
- package/src/plugins/rm/README.md +1 -1
- package/src/plugins/rm/rm.js +58 -14
- package/src/plugins/rm/rmDoc.js +2 -24
- package/src/plugins/rm/rmDoc.md +13 -0
- package/src/plugins/rpc/README.md +2 -2
- package/src/plugins/rpc/rpc.js +515 -296
- package/src/plugins/set/README.md +1 -1
- package/src/plugins/set/set.js +318 -77
- package/src/plugins/set/setDoc.js +2 -35
- package/src/plugins/set/setDoc.md +22 -0
- package/src/plugins/sh/README.md +28 -5
- package/src/plugins/sh/sh.js +52 -8
- package/src/plugins/sh/shDoc.js +2 -23
- package/src/plugins/sh/shDoc.md +13 -0
- package/src/plugins/skill/README.md +23 -0
- package/src/plugins/skill/skill.js +14 -17
- package/src/plugins/stream/README.md +101 -0
- package/src/plugins/stream/stream.js +290 -0
- package/src/plugins/telemetry/README.md +1 -1
- package/src/plugins/telemetry/telemetry.js +148 -74
- package/src/plugins/think/README.md +1 -1
- package/src/plugins/think/think.js +14 -1
- package/src/plugins/think/thinkDoc.js +2 -17
- package/src/plugins/think/thinkDoc.md +7 -0
- package/src/plugins/unknown/README.md +3 -3
- package/src/plugins/unknown/unknown.js +56 -21
- package/src/plugins/unknown/unknownDoc.js +2 -25
- package/src/plugins/unknown/unknownDoc.md +11 -0
- package/src/plugins/update/README.md +1 -1
- package/src/plugins/update/update.js +67 -5
- package/src/plugins/update/updateDoc.js +2 -27
- package/src/plugins/update/updateDoc.md +8 -0
- package/src/plugins/xai/README.md +23 -0
- package/src/{llm/XaiClient.js → plugins/xai/xai.js} +58 -37
- package/src/server/ClientConnection.js +64 -37
- package/src/server/SocketServer.js +23 -10
- package/src/server/protocol.js +11 -0
- package/src/sql/functions/slugify.js +13 -1
- package/src/sql/v_model_context.sql +27 -31
- package/src/sql/v_run_log.sql +9 -14
- package/EXCEPTIONS.md +0 -46
- package/src/agent/KnownStore.js +0 -338
- package/src/agent/ResponseHealer.js +0 -188
- package/src/llm/OpenAiClient.js +0 -100
- package/src/llm/OpenRouterClient.js +0 -100
- package/src/plugins/budget/recovery.js +0 -47
- package/src/plugins/instructions/preamble.md +0 -37
- package/src/plugins/performed/README.md +0 -15
- package/src/plugins/performed/performed.js +0 -45
- package/src/plugins/previous/README.md +0 -16
- package/src/plugins/previous/previous.js +0 -60
- package/src/plugins/progress/README.md +0 -16
- package/src/plugins/progress/progress.js +0 -26
- package/src/plugins/summarize/README.md +0 -19
- package/src/plugins/summarize/summarize.js +0 -32
- package/src/plugins/summarize/summarizeDoc.js +0 -28
package/src/hooks/Hooks.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import HookRegistry from "./HookRegistry.js";
|
|
2
|
+
import RpcRegistry from "./RpcRegistry.js";
|
|
2
3
|
import ToolRegistry from "./ToolRegistry.js";
|
|
3
4
|
|
|
4
5
|
/**
|
|
@@ -56,10 +57,25 @@ export default function createHooks(debug = false) {
|
|
|
56
57
|
turn: {
|
|
57
58
|
started: createEvent("turn.started"),
|
|
58
59
|
response: createEvent("turn.response"),
|
|
59
|
-
proposal: createEvent("turn.proposal"),
|
|
60
|
-
proposing: createEvent("turn.proposing"),
|
|
61
60
|
completed: createEvent("turn.completed"),
|
|
62
61
|
},
|
|
62
|
+
proposal: {
|
|
63
|
+
prepare: createEvent("proposal.prepare"),
|
|
64
|
+
pending: createEvent("proposal.pending"),
|
|
65
|
+
// Plugins veto acceptance by returning {allow:false, outcome, body}.
|
|
66
|
+
// Used e.g. by set plugin's readonly constraint check.
|
|
67
|
+
accepting: createFilter("proposal.accepting"),
|
|
68
|
+
// Plugins compose the resolved body based on path/action. Default
|
|
69
|
+
// is output || "". Used e.g. by set plugin to preserve the
|
|
70
|
+
// model's proposed content as the resolved body.
|
|
71
|
+
content: createFilter("proposal.content"),
|
|
72
|
+
// Fires after a proposal resolves with action="accept". Plugins
|
|
73
|
+
// perform their side effects (file materialize, unlink, stream
|
|
74
|
+
// setup, etc.) here — NOT in AgentLoop.resolve.
|
|
75
|
+
accepted: createEvent("proposal.accepted"),
|
|
76
|
+
// Fires after a proposal resolves with action="error" or "reject".
|
|
77
|
+
rejected: createEvent("proposal.rejected"),
|
|
78
|
+
},
|
|
63
79
|
assembly: {
|
|
64
80
|
system: createFilter("assembly.system"),
|
|
65
81
|
user: createFilter("assembly.user"),
|
|
@@ -82,6 +98,25 @@ export default function createHooks(debug = false) {
|
|
|
82
98
|
},
|
|
83
99
|
messages: createFilter("llm.messages"),
|
|
84
100
|
response: createFilter("llm.response"),
|
|
101
|
+
// Reasoning merge filter. Subscribers contribute per-tag
|
|
102
|
+
// reasoning text (e.g. the think plugin's <think>…</think>)
|
|
103
|
+
// to the model's reasoning_content field. Fires between parse
|
|
104
|
+
// and turn.response.
|
|
105
|
+
reasoning: createFilter("llm.reasoning"),
|
|
106
|
+
// LLM provider registry. Plugins contribute entries shaped:
|
|
107
|
+
// {
|
|
108
|
+
// name: string,
|
|
109
|
+
// matches: (modelAlias) => boolean,
|
|
110
|
+
// completion: (messages, modelAlias, options) => Promise<response>,
|
|
111
|
+
// getContextSize: (modelAlias) => Promise<number>,
|
|
112
|
+
// }
|
|
113
|
+
// Each provider owns a prefix namespace (e.g. "openai/", "ollama/",
|
|
114
|
+
// "openrouter/"). LlmProvider picks the first provider whose
|
|
115
|
+
// matches() returns true. No catchall — if a model alias doesn't
|
|
116
|
+
// match any registered provider, the request fails with a clear
|
|
117
|
+
// "no provider registered" error. External plugins add new
|
|
118
|
+
// prefixes without namespace collision.
|
|
119
|
+
providers: [],
|
|
85
120
|
},
|
|
86
121
|
file: {},
|
|
87
122
|
prompt: {
|
|
@@ -100,6 +135,12 @@ export default function createHooks(debug = false) {
|
|
|
100
135
|
materialized: createEvent("context.materialized"),
|
|
101
136
|
},
|
|
102
137
|
action: {},
|
|
138
|
+
error: {
|
|
139
|
+
log: createEvent("error.log"),
|
|
140
|
+
},
|
|
141
|
+
stream: {
|
|
142
|
+
cancelled: createEvent("stream.cancelled"),
|
|
143
|
+
},
|
|
103
144
|
ui: {
|
|
104
145
|
render: createEvent("ui.render"),
|
|
105
146
|
notify: createEvent("ui.notify"),
|
|
@@ -117,7 +158,7 @@ export default function createHooks(debug = false) {
|
|
|
117
158
|
response: {
|
|
118
159
|
result: createFilter("rpc.response.result"),
|
|
119
160
|
},
|
|
120
|
-
registry:
|
|
161
|
+
registry: new RpcRegistry(),
|
|
121
162
|
},
|
|
122
163
|
agent: {},
|
|
123
164
|
tools,
|
|
@@ -10,8 +10,6 @@
|
|
|
10
10
|
export default class PluginContext {
|
|
11
11
|
#name;
|
|
12
12
|
#hooks;
|
|
13
|
-
#db = null;
|
|
14
|
-
#store = null;
|
|
15
13
|
|
|
16
14
|
constructor(name, hooks) {
|
|
17
15
|
this.#name = name;
|
|
@@ -22,22 +20,6 @@ export default class PluginContext {
|
|
|
22
20
|
return this.#name;
|
|
23
21
|
}
|
|
24
22
|
|
|
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
23
|
#schemes = [];
|
|
42
24
|
|
|
43
25
|
get hooks() {
|
|
@@ -48,16 +30,36 @@ export default class PluginContext {
|
|
|
48
30
|
return this.#schemes;
|
|
49
31
|
}
|
|
50
32
|
|
|
51
|
-
registerScheme({
|
|
33
|
+
registerScheme({
|
|
34
|
+
name,
|
|
35
|
+
modelVisible = 1,
|
|
36
|
+
category = "logging",
|
|
37
|
+
scope = "run",
|
|
38
|
+
writableBy = ["model", "plugin"],
|
|
39
|
+
} = {}) {
|
|
52
40
|
if (!PluginContext.CATEGORIES.has(category)) {
|
|
53
41
|
throw new Error(
|
|
54
42
|
`Invalid category "${category}". Must be one of: ${[...PluginContext.CATEGORIES].join(", ")}`,
|
|
55
43
|
);
|
|
56
44
|
}
|
|
45
|
+
if (!PluginContext.SCOPES.has(scope)) {
|
|
46
|
+
throw new Error(
|
|
47
|
+
`Invalid scope "${scope}". Must be one of: ${[...PluginContext.SCOPES].join(", ")}`,
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
for (const w of writableBy) {
|
|
51
|
+
if (!PluginContext.WRITERS.has(w)) {
|
|
52
|
+
throw new Error(
|
|
53
|
+
`Invalid writer "${w}" in writableBy. Must be one of: ${[...PluginContext.WRITERS].join(", ")}`,
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
57
|
this.#schemes.push({
|
|
58
58
|
name: name || this.#name,
|
|
59
59
|
model_visible: modelVisible,
|
|
60
60
|
category,
|
|
61
|
+
default_scope: scope,
|
|
62
|
+
writable_by: JSON.stringify(writableBy),
|
|
61
63
|
});
|
|
62
64
|
}
|
|
63
65
|
|
|
@@ -65,14 +67,26 @@ export default class PluginContext {
|
|
|
65
67
|
new Set(["data", "logging", "unknown", "prompt"]),
|
|
66
68
|
);
|
|
67
69
|
|
|
70
|
+
static SCOPES = Object.freeze(new Set(["run", "project", "global"]));
|
|
71
|
+
|
|
72
|
+
static WRITERS = Object.freeze(
|
|
73
|
+
new Set(["model", "plugin", "client", "system"]),
|
|
74
|
+
);
|
|
75
|
+
|
|
68
76
|
ensureTool() {
|
|
69
77
|
this.#hooks.tools.ensureTool(this.#name);
|
|
70
78
|
}
|
|
71
79
|
|
|
80
|
+
// Mark this plugin's tool as hidden from model-facing tool lists.
|
|
81
|
+
// Handler still dispatches if the model emits the tag.
|
|
82
|
+
markHidden() {
|
|
83
|
+
this.#hooks.tools.markHidden(this.#name);
|
|
84
|
+
}
|
|
85
|
+
|
|
72
86
|
/**
|
|
73
87
|
* Register a named callback for this plugin.
|
|
74
88
|
* "handler" registers the tool handler.
|
|
75
|
-
* "
|
|
89
|
+
* "visible"/"summarized" register visibility projections.
|
|
76
90
|
* "docs" sets tool documentation.
|
|
77
91
|
* Everything else resolves to a hook event.
|
|
78
92
|
*/
|
|
@@ -82,7 +96,7 @@ export default class PluginContext {
|
|
|
82
96
|
this.#hooks.tools.onHandle(this.#name, callback, priority);
|
|
83
97
|
return;
|
|
84
98
|
}
|
|
85
|
-
if (event === "
|
|
99
|
+
if (event === "visible" || event === "summarized") {
|
|
86
100
|
this.#hooks.tools.onView(this.#name, callback, event);
|
|
87
101
|
return;
|
|
88
102
|
}
|
|
@@ -57,7 +57,8 @@ export default class RpcRegistry {
|
|
|
57
57
|
if (!params.path) throw new Error("path is required");
|
|
58
58
|
if (!params.run) throw new Error("run is required");
|
|
59
59
|
const { rummy } = await buildRunContext(hooks, ctx, params.run);
|
|
60
|
-
|
|
60
|
+
const { body = "" } = params;
|
|
61
|
+
await dispatchTool(hooks, rummy, name, params.path, body, {
|
|
61
62
|
path: params.path,
|
|
62
63
|
to: params.to,
|
|
63
64
|
...params.attributes,
|
|
@@ -2,17 +2,40 @@
|
|
|
2
2
|
* RummyContext provides a unified, semantic API for plugins to interact with
|
|
3
3
|
* the Turn node tree and core resources like the Database and Project metadata.
|
|
4
4
|
*/
|
|
5
|
+
// Entries write verbs that should automatically carry the caller's
|
|
6
|
+
// writer identity. Handler-issued writes on behalf of the model default
|
|
7
|
+
// to writer=model; plugin background writes (set via rummy from a hook
|
|
8
|
+
// with writer: "plugin" or "system" in ctx) get the context's writer.
|
|
9
|
+
const WRITE_VERBS = new Set(["set", "rm", "cp", "mv", "update"]);
|
|
10
|
+
|
|
11
|
+
// Defaults applied at construction so every plugin-facing getter
|
|
12
|
+
// returns a predictable shape without per-access fallbacks.
|
|
13
|
+
const CONTEXT_DEFAULTS = Object.freeze({
|
|
14
|
+
hooks: null,
|
|
15
|
+
activeFiles: [],
|
|
16
|
+
sequence: 0,
|
|
17
|
+
runId: null,
|
|
18
|
+
turnId: null,
|
|
19
|
+
loopId: null,
|
|
20
|
+
toolSet: null,
|
|
21
|
+
contextSize: null,
|
|
22
|
+
systemPrompt: "",
|
|
23
|
+
loopPrompt: "",
|
|
24
|
+
writer: "model",
|
|
25
|
+
});
|
|
26
|
+
|
|
5
27
|
export default class RummyContext {
|
|
6
28
|
#root;
|
|
7
29
|
#context;
|
|
30
|
+
#wrappedStore;
|
|
8
31
|
|
|
9
32
|
constructor(root, context) {
|
|
10
33
|
this.#root = root;
|
|
11
|
-
this.#context = context;
|
|
34
|
+
this.#context = { ...CONTEXT_DEFAULTS, ...context };
|
|
12
35
|
}
|
|
13
36
|
|
|
14
37
|
get hooks() {
|
|
15
|
-
return this.#context.hooks
|
|
38
|
+
return this.#context.hooks;
|
|
16
39
|
}
|
|
17
40
|
|
|
18
41
|
get db() {
|
|
@@ -20,7 +43,19 @@ export default class RummyContext {
|
|
|
20
43
|
}
|
|
21
44
|
|
|
22
45
|
get entries() {
|
|
23
|
-
|
|
46
|
+
if (this.#wrappedStore) return this.#wrappedStore;
|
|
47
|
+
const store = this.#context.store;
|
|
48
|
+
if (!store) return null;
|
|
49
|
+
const writer = this.writer;
|
|
50
|
+
this.#wrappedStore = new Proxy(store, {
|
|
51
|
+
get(target, prop) {
|
|
52
|
+
const val = target[prop];
|
|
53
|
+
if (typeof val !== "function") return val;
|
|
54
|
+
if (!WRITE_VERBS.has(prop)) return val.bind(target);
|
|
55
|
+
return (args = {}) => val.call(target, { writer, ...args });
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
return this.#wrappedStore;
|
|
24
59
|
}
|
|
25
60
|
|
|
26
61
|
get project() {
|
|
@@ -28,7 +63,7 @@ export default class RummyContext {
|
|
|
28
63
|
}
|
|
29
64
|
|
|
30
65
|
get activeFiles() {
|
|
31
|
-
return this.#context.activeFiles
|
|
66
|
+
return this.#context.activeFiles;
|
|
32
67
|
}
|
|
33
68
|
|
|
34
69
|
get type() {
|
|
@@ -40,19 +75,19 @@ export default class RummyContext {
|
|
|
40
75
|
}
|
|
41
76
|
|
|
42
77
|
get sequence() {
|
|
43
|
-
return this.#context.sequence
|
|
78
|
+
return this.#context.sequence;
|
|
44
79
|
}
|
|
45
80
|
|
|
46
81
|
get runId() {
|
|
47
|
-
return this.#context.runId
|
|
82
|
+
return this.#context.runId;
|
|
48
83
|
}
|
|
49
84
|
|
|
50
85
|
get turnId() {
|
|
51
|
-
return this.#context.turnId
|
|
86
|
+
return this.#context.turnId;
|
|
52
87
|
}
|
|
53
88
|
|
|
54
89
|
get loopId() {
|
|
55
|
-
return this.#context.loopId
|
|
90
|
+
return this.#context.loopId;
|
|
56
91
|
}
|
|
57
92
|
|
|
58
93
|
get noRepo() {
|
|
@@ -67,20 +102,34 @@ export default class RummyContext {
|
|
|
67
102
|
return this.#context.noWeb === true;
|
|
68
103
|
}
|
|
69
104
|
|
|
105
|
+
get noProposals() {
|
|
106
|
+
return this.#context.noProposals === true;
|
|
107
|
+
}
|
|
108
|
+
|
|
70
109
|
get toolSet() {
|
|
71
|
-
return this.#context.toolSet
|
|
110
|
+
return this.#context.toolSet;
|
|
72
111
|
}
|
|
73
112
|
|
|
74
113
|
get contextSize() {
|
|
75
|
-
return this.#context.contextSize
|
|
114
|
+
return this.#context.contextSize;
|
|
76
115
|
}
|
|
77
116
|
|
|
78
117
|
get systemPrompt() {
|
|
79
|
-
return this.#context.systemPrompt
|
|
118
|
+
return this.#context.systemPrompt;
|
|
80
119
|
}
|
|
81
120
|
|
|
82
121
|
get loopPrompt() {
|
|
83
|
-
return this.#context.loopPrompt
|
|
122
|
+
return this.#context.loopPrompt;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Writer identity for Entries permission checks. Defaults to
|
|
127
|
+
* 'model' — handlers write on behalf of the model's emitted command.
|
|
128
|
+
* Non-handler plugin code (streaming callbacks, background emissions)
|
|
129
|
+
* passes `writer: 'plugin'` or `'system'` explicitly.
|
|
130
|
+
*/
|
|
131
|
+
get writer() {
|
|
132
|
+
return this.#context.writer;
|
|
84
133
|
}
|
|
85
134
|
|
|
86
135
|
get system() {
|
|
@@ -101,48 +150,83 @@ export default class RummyContext {
|
|
|
101
150
|
|
|
102
151
|
// --- Tool methods (same operations the model uses) ---
|
|
103
152
|
|
|
104
|
-
async set({
|
|
153
|
+
async set({
|
|
154
|
+
path,
|
|
155
|
+
body = "",
|
|
156
|
+
state = "resolved",
|
|
157
|
+
outcome = null,
|
|
158
|
+
visibility,
|
|
159
|
+
attributes,
|
|
160
|
+
} = {}) {
|
|
105
161
|
if (!path) {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
162
|
+
path = await this.entries.slugPath(
|
|
163
|
+
this.runId,
|
|
164
|
+
"known",
|
|
165
|
+
body,
|
|
166
|
+
attributes?.summary,
|
|
167
|
+
);
|
|
109
168
|
}
|
|
110
|
-
await this.entries.
|
|
111
|
-
this.runId,
|
|
112
|
-
this.sequence,
|
|
169
|
+
await this.entries.set({
|
|
170
|
+
runId: this.runId,
|
|
171
|
+
turn: this.sequence,
|
|
113
172
|
path,
|
|
114
|
-
body
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
173
|
+
body,
|
|
174
|
+
state,
|
|
175
|
+
outcome,
|
|
176
|
+
visibility,
|
|
177
|
+
attributes,
|
|
178
|
+
loopId: this.loopId,
|
|
179
|
+
});
|
|
118
180
|
return path;
|
|
119
181
|
}
|
|
120
182
|
|
|
121
183
|
async get(path) {
|
|
122
|
-
await this.entries.
|
|
184
|
+
await this.entries.get({
|
|
185
|
+
runId: this.runId,
|
|
186
|
+
turn: this.sequence,
|
|
187
|
+
path: path,
|
|
188
|
+
bodyFilter: null,
|
|
189
|
+
});
|
|
123
190
|
}
|
|
124
191
|
|
|
125
|
-
async
|
|
126
|
-
await this.entries.
|
|
192
|
+
async rm(path) {
|
|
193
|
+
await this.entries.rm({ runId: this.runId, path: path });
|
|
127
194
|
}
|
|
128
195
|
|
|
129
|
-
async
|
|
130
|
-
|
|
196
|
+
async update(body, { status = 102, attributes = {} } = {}) {
|
|
197
|
+
return this.entries.update({
|
|
198
|
+
runId: this.runId,
|
|
199
|
+
turn: this.sequence,
|
|
200
|
+
body,
|
|
201
|
+
status,
|
|
202
|
+
attributes,
|
|
203
|
+
loopId: this.loopId,
|
|
204
|
+
});
|
|
131
205
|
}
|
|
132
206
|
|
|
133
207
|
async mv(from, to) {
|
|
134
208
|
const body = await this.entries.getBody(this.runId, from);
|
|
135
209
|
if (body === null) return;
|
|
136
|
-
await this.entries.
|
|
210
|
+
await this.entries.set({
|
|
211
|
+
runId: this.runId,
|
|
212
|
+
turn: this.sequence,
|
|
213
|
+
path: to,
|
|
214
|
+
body,
|
|
215
|
+
state: "resolved",
|
|
137
216
|
loopId: this.loopId,
|
|
138
217
|
});
|
|
139
|
-
await this.entries.
|
|
218
|
+
await this.entries.rm({ runId: this.runId, path: from });
|
|
140
219
|
}
|
|
141
220
|
|
|
142
221
|
async cp(from, to) {
|
|
143
222
|
const body = await this.entries.getBody(this.runId, from);
|
|
144
223
|
if (body === null) return;
|
|
145
|
-
await this.entries.
|
|
224
|
+
await this.entries.set({
|
|
225
|
+
runId: this.runId,
|
|
226
|
+
turn: this.sequence,
|
|
227
|
+
path: to,
|
|
228
|
+
body,
|
|
229
|
+
state: "resolved",
|
|
146
230
|
loopId: this.loopId,
|
|
147
231
|
});
|
|
148
232
|
}
|
|
@@ -157,9 +241,16 @@ export default class RummyContext {
|
|
|
157
241
|
return this.entries.getAttributes(this.runId, path);
|
|
158
242
|
}
|
|
159
243
|
|
|
160
|
-
async
|
|
244
|
+
async getState(path) {
|
|
161
245
|
const row = await this.entries.getState(this.runId, path);
|
|
162
|
-
|
|
246
|
+
if (!row) return null;
|
|
247
|
+
return row.state;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
async getOutcome(path) {
|
|
251
|
+
const row = await this.entries.getState(this.runId, path);
|
|
252
|
+
if (!row) return null;
|
|
253
|
+
return row.outcome;
|
|
163
254
|
}
|
|
164
255
|
|
|
165
256
|
async getEntry(path) {
|
|
@@ -168,11 +259,16 @@ export default class RummyContext {
|
|
|
168
259
|
path,
|
|
169
260
|
null,
|
|
170
261
|
);
|
|
171
|
-
|
|
262
|
+
if (results.length === 0) return null;
|
|
263
|
+
return results[0];
|
|
172
264
|
}
|
|
173
265
|
|
|
174
266
|
async setAttributes(path, attrs) {
|
|
175
|
-
return this.entries.
|
|
267
|
+
return this.entries.set({
|
|
268
|
+
runId: this.runId,
|
|
269
|
+
path: path,
|
|
270
|
+
attributes: attrs,
|
|
271
|
+
});
|
|
176
272
|
}
|
|
177
273
|
|
|
178
274
|
async getEntries(pattern, bodyFilter) {
|
|
@@ -181,7 +277,13 @@ export default class RummyContext {
|
|
|
181
277
|
|
|
182
278
|
async log(message) {
|
|
183
279
|
const path = `content://${Date.now()}`;
|
|
184
|
-
await this.entries.
|
|
280
|
+
await this.entries.set({
|
|
281
|
+
runId: this.runId,
|
|
282
|
+
turn: this.sequence,
|
|
283
|
+
path,
|
|
284
|
+
body: message,
|
|
285
|
+
state: "resolved",
|
|
286
|
+
});
|
|
185
287
|
}
|
|
186
288
|
|
|
187
289
|
// --- Node tree methods ---
|
|
@@ -191,7 +293,8 @@ export default class RummyContext {
|
|
|
191
293
|
const childArray = Array.isArray(children) ? children : [children];
|
|
192
294
|
for (const child of childArray) {
|
|
193
295
|
if (typeof child === "string") {
|
|
194
|
-
node.content
|
|
296
|
+
if (node.content === null) node.content = "";
|
|
297
|
+
node.content += child;
|
|
195
298
|
} else if (child && typeof child === "object") {
|
|
196
299
|
node.children.push(child);
|
|
197
300
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Tool display order: gather → reason → act → communicate.
|
|
2
2
|
// Position in the list implies priority to the model.
|
|
3
|
+
// `update` is pinned last — it's the turn-closer, not an action.
|
|
3
4
|
const TOOL_ORDER = [
|
|
4
5
|
"think",
|
|
5
6
|
"unknown",
|
|
@@ -12,18 +13,18 @@ const TOOL_ORDER = [
|
|
|
12
13
|
"cp",
|
|
13
14
|
"mv",
|
|
14
15
|
"ask_user",
|
|
15
|
-
"update",
|
|
16
|
-
"summarize",
|
|
17
16
|
"search",
|
|
18
17
|
];
|
|
19
18
|
|
|
20
19
|
function sortByPriority(names) {
|
|
21
20
|
return names.toSorted((a, b) => {
|
|
21
|
+
if (a === "update") return 1;
|
|
22
|
+
if (b === "update") return -1;
|
|
22
23
|
const ia = TOOL_ORDER.indexOf(a);
|
|
23
24
|
const ib = TOOL_ORDER.indexOf(b);
|
|
24
25
|
if (ia === -1 && ib === -1) return a.localeCompare(b);
|
|
25
26
|
if (ia === -1) return 1;
|
|
26
|
-
if (ib === -1) return 1;
|
|
27
|
+
if (ib === -1) return -1;
|
|
27
28
|
return ia - ib;
|
|
28
29
|
});
|
|
29
30
|
}
|
|
@@ -32,12 +33,20 @@ export default class ToolRegistry {
|
|
|
32
33
|
#tools = new Map();
|
|
33
34
|
#handlers = new Map();
|
|
34
35
|
#views = new Map();
|
|
36
|
+
#hidden = new Set();
|
|
35
37
|
|
|
36
38
|
ensureTool(scheme) {
|
|
37
39
|
if (this.#tools.has(scheme)) return;
|
|
38
40
|
this.#tools.set(scheme, Object.freeze({}));
|
|
39
41
|
}
|
|
40
42
|
|
|
43
|
+
// Hidden tools dispatch on direct emission but don't appear in any
|
|
44
|
+
// model-facing tool list. Internal schemes (e.g. <known>, <unknown>)
|
|
45
|
+
// the model writes via <set path="scheme://..."> instead.
|
|
46
|
+
markHidden(scheme) {
|
|
47
|
+
this.#hidden.add(scheme);
|
|
48
|
+
}
|
|
49
|
+
|
|
41
50
|
get(name) {
|
|
42
51
|
return this.#tools.get(name);
|
|
43
52
|
}
|
|
@@ -53,51 +62,35 @@ export default class ToolRegistry {
|
|
|
53
62
|
list.sort((a, b) => a.priority - b.priority);
|
|
54
63
|
}
|
|
55
64
|
|
|
56
|
-
onView(scheme, fn,
|
|
65
|
+
onView(scheme, fn, visibility = "visible") {
|
|
57
66
|
if (!this.#views.has(scheme)) this.#views.set(scheme, new Map());
|
|
58
|
-
this.#views.get(scheme).set(
|
|
67
|
+
this.#views.get(scheme).set(visibility, fn);
|
|
59
68
|
}
|
|
60
69
|
|
|
61
70
|
async view(scheme, entry) {
|
|
62
|
-
const
|
|
63
|
-
if (!
|
|
71
|
+
const visibilityMap = this.#views.get(scheme);
|
|
72
|
+
if (!visibilityMap) {
|
|
64
73
|
throw new Error(
|
|
65
74
|
`No view registered for scheme '${scheme}'. ` +
|
|
66
75
|
`Every tool must define how its entries appear in the model view.`,
|
|
67
76
|
);
|
|
68
77
|
}
|
|
69
78
|
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
const summary = typeof attrs?.summary === "string" ? attrs.summary : null;
|
|
75
|
-
|
|
76
|
-
const fidelity = entry.fidelity || "full";
|
|
77
|
-
const fn = fidelityMap.get(fidelity);
|
|
78
|
-
if (!fn) {
|
|
79
|
-
// No view for this fidelity — fall back on model-authored summary
|
|
80
|
-
return summary || "";
|
|
81
|
-
}
|
|
79
|
+
const visibility =
|
|
80
|
+
entry.visibility === undefined ? "visible" : entry.visibility;
|
|
81
|
+
const fn = visibilityMap.get(visibility);
|
|
82
|
+
if (!fn) return "";
|
|
82
83
|
|
|
83
84
|
const body = await fn(entry);
|
|
84
|
-
|
|
85
|
-
//
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// Fall back to summary attribute when plugin returns empty
|
|
91
|
-
if (fidelity === "summary" && summary && !body) {
|
|
92
|
-
return summary;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
return body;
|
|
85
|
+
// View handlers MAY return undefined or null to mean "no projected
|
|
86
|
+
// body at this visibility" — normalize at this boundary so callers
|
|
87
|
+
// get a predictable string.
|
|
88
|
+
return body == null ? "" : body;
|
|
96
89
|
}
|
|
97
90
|
|
|
98
91
|
hasView(scheme) {
|
|
99
|
-
const
|
|
100
|
-
return
|
|
92
|
+
const visibilityMap = this.#views.get(scheme);
|
|
93
|
+
return visibilityMap?.size > 0;
|
|
101
94
|
}
|
|
102
95
|
|
|
103
96
|
async dispatch(scheme, entry, rummy) {
|
|
@@ -113,15 +106,23 @@ export default class ToolRegistry {
|
|
|
113
106
|
return sortByPriority([...this.#tools.keys()]);
|
|
114
107
|
}
|
|
115
108
|
|
|
109
|
+
// Names advertised to the model — registered tools minus hidden ones.
|
|
110
|
+
// Use this anywhere a tool list is shown to the model.
|
|
111
|
+
get advertisedNames() {
|
|
112
|
+
return sortByPriority(
|
|
113
|
+
[...this.#tools.keys()].filter((n) => !this.#hidden.has(n)),
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
116
117
|
/**
|
|
117
118
|
* Compute the active tool set for a loop.
|
|
118
|
-
* All exclusions — mode, flags — handled here. One mechanism.
|
|
119
|
+
* All exclusions — mode, flags, hidden — handled here. One mechanism.
|
|
119
120
|
*/
|
|
120
121
|
resolveForLoop(
|
|
121
122
|
mode,
|
|
122
123
|
{ noInteraction = false, noWeb = false, noProposals = false } = {},
|
|
123
124
|
) {
|
|
124
|
-
const excluded = new Set();
|
|
125
|
+
const excluded = new Set(this.#hidden);
|
|
125
126
|
if (mode === "ask") excluded.add("sh");
|
|
126
127
|
if (noInteraction) excluded.add("ask_user");
|
|
127
128
|
if (noWeb) excluded.add("search");
|