@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
|
@@ -1,17 +1,23 @@
|
|
|
1
|
-
import { writeFile } from "node:fs/promises";
|
|
1
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
|
|
4
4
|
export default class Telemetry {
|
|
5
5
|
#core;
|
|
6
6
|
#starts = new Map();
|
|
7
7
|
#lastRunPath = null;
|
|
8
|
+
#turnsDir = null;
|
|
8
9
|
#turnLog = [];
|
|
10
|
+
#currentRunAlias = null;
|
|
11
|
+
#currentTurn = null;
|
|
9
12
|
|
|
10
13
|
constructor(core) {
|
|
11
14
|
this.#core = core;
|
|
12
15
|
|
|
13
16
|
const home = process.env.RUMMY_HOME;
|
|
14
|
-
if (home)
|
|
17
|
+
if (home) {
|
|
18
|
+
this.#lastRunPath = join(home, "last_run.txt");
|
|
19
|
+
this.#turnsDir = join(home, "turns");
|
|
20
|
+
}
|
|
15
21
|
|
|
16
22
|
core.on("rpc.started", this.#onRpcStarted.bind(this));
|
|
17
23
|
core.on("rpc.completed", this.#onRpcCompleted.bind(this));
|
|
@@ -24,14 +30,15 @@ export default class Telemetry {
|
|
|
24
30
|
|
|
25
31
|
async #onRpcStarted({ method, id, params }) {
|
|
26
32
|
this.#starts.set(id, Date.now());
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
let summary = "";
|
|
34
|
+
if (method === "ask" || method === "act") {
|
|
35
|
+
const prompt = params?.prompt ? params.prompt : "";
|
|
36
|
+
summary = `prompt="${prompt.slice(0, 60)}"`;
|
|
37
|
+
} else if (method === "run/abort") {
|
|
38
|
+
summary = `run=${params?.run}`;
|
|
39
|
+
} else if (method === "run/resolve") {
|
|
40
|
+
summary = `run=${params?.run} action=${params?.resolution?.action}`;
|
|
41
|
+
}
|
|
35
42
|
console.log(`[RPC] → ${method}(${id})${summary ? ` ${summary}` : ""}`);
|
|
36
43
|
|
|
37
44
|
if (method === "ask" || method === "act") {
|
|
@@ -44,11 +51,13 @@ export default class Telemetry {
|
|
|
44
51
|
? `${((Date.now() - this.#starts.get(id)) / 1000).toFixed(1)}s`
|
|
45
52
|
: "";
|
|
46
53
|
this.#starts.delete(id);
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
54
|
+
let summary = "";
|
|
55
|
+
if (result?.run) {
|
|
56
|
+
const status = result.status ? result.status : "ok";
|
|
57
|
+
summary = `run=${result.run} status=${status}`;
|
|
58
|
+
} else if (result?.status) {
|
|
59
|
+
summary = `status=${result.status}`;
|
|
60
|
+
}
|
|
52
61
|
console.log(
|
|
53
62
|
`[RPC] ← ${method}(${id}) ${elapsed}${summary ? ` ${summary}` : ""}`,
|
|
54
63
|
);
|
|
@@ -59,7 +68,8 @@ export default class Telemetry {
|
|
|
59
68
|
? `${((Date.now() - this.#starts.get(id)) / 1000).toFixed(1)}s`
|
|
60
69
|
: "";
|
|
61
70
|
this.#starts.delete(id);
|
|
62
|
-
|
|
71
|
+
const detail = error?.message ? error.message : error;
|
|
72
|
+
console.error(`[RPC] ✗ (${id}) ${elapsed} ${detail}`);
|
|
63
73
|
}
|
|
64
74
|
|
|
65
75
|
async #onStepCompleted(payload) {
|
|
@@ -81,95 +91,150 @@ export default class Telemetry {
|
|
|
81
91
|
userMsg,
|
|
82
92
|
}) {
|
|
83
93
|
const { entries: store, runId, loopId } = rummy;
|
|
94
|
+
// Audit schemes are system-only writes (see initPlugins).
|
|
95
|
+
const systemOpts = { loopId, visibility: "archived", writer: "system" };
|
|
84
96
|
|
|
85
97
|
// assistant://N — the model's raw response
|
|
86
|
-
await store.
|
|
87
|
-
|
|
88
|
-
|
|
98
|
+
await store.set({
|
|
99
|
+
runId,
|
|
100
|
+
turn,
|
|
101
|
+
path: `assistant://${turn}`,
|
|
102
|
+
body: content,
|
|
103
|
+
state: "resolved",
|
|
104
|
+
...systemOpts,
|
|
89
105
|
});
|
|
90
106
|
|
|
91
107
|
// system://N, user://N — assembled messages as audit
|
|
92
108
|
if (systemMsg) {
|
|
93
|
-
await store.
|
|
94
|
-
|
|
95
|
-
|
|
109
|
+
await store.set({
|
|
110
|
+
runId,
|
|
111
|
+
turn,
|
|
112
|
+
path: `system://${turn}`,
|
|
113
|
+
body: systemMsg,
|
|
114
|
+
state: "resolved",
|
|
115
|
+
...systemOpts,
|
|
96
116
|
});
|
|
97
117
|
}
|
|
98
118
|
if (userMsg) {
|
|
99
|
-
await store.
|
|
100
|
-
|
|
101
|
-
|
|
119
|
+
await store.set({
|
|
120
|
+
runId,
|
|
121
|
+
turn,
|
|
122
|
+
path: `user://${turn}`,
|
|
123
|
+
body: userMsg,
|
|
124
|
+
state: "resolved",
|
|
125
|
+
...systemOpts,
|
|
102
126
|
});
|
|
103
127
|
}
|
|
104
128
|
|
|
105
129
|
// model://N — raw API response diagnostics
|
|
106
|
-
await store.
|
|
130
|
+
await store.set({
|
|
107
131
|
runId,
|
|
108
132
|
turn,
|
|
109
|
-
`model://${turn}`,
|
|
110
|
-
JSON.stringify({
|
|
133
|
+
path: `model://${turn}`,
|
|
134
|
+
body: JSON.stringify({
|
|
111
135
|
keys: responseMessage ? Object.keys(responseMessage) : [],
|
|
112
|
-
reasoning_content: responseMessage?.reasoning_content
|
|
136
|
+
reasoning_content: responseMessage?.reasoning_content
|
|
137
|
+
? responseMessage.reasoning_content
|
|
138
|
+
: null,
|
|
113
139
|
content: content.slice(0, 4096),
|
|
114
|
-
usage: result.usage
|
|
115
|
-
model: result.model
|
|
140
|
+
usage: result.usage ? result.usage : null,
|
|
141
|
+
model: result.model ? result.model : null,
|
|
116
142
|
}),
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
);
|
|
143
|
+
state: "resolved",
|
|
144
|
+
...systemOpts,
|
|
145
|
+
});
|
|
120
146
|
|
|
121
147
|
// reasoning://N
|
|
122
148
|
if (responseMessage?.reasoning_content) {
|
|
123
|
-
await store.
|
|
149
|
+
await store.set({
|
|
124
150
|
runId,
|
|
125
151
|
turn,
|
|
126
|
-
`reasoning://${turn}`,
|
|
127
|
-
responseMessage.reasoning_content,
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
);
|
|
152
|
+
path: `reasoning://${turn}`,
|
|
153
|
+
body: responseMessage.reasoning_content,
|
|
154
|
+
state: "resolved",
|
|
155
|
+
...systemOpts,
|
|
156
|
+
});
|
|
157
|
+
if (process.env.RUMMY_DEBUG === "true") {
|
|
158
|
+
console.log(
|
|
159
|
+
`\n--- REASONING turn ${turn} (${responseMessage.reasoning_content.length} chars) ---\n${responseMessage.reasoning_content}\n--- END REASONING turn ${turn} ---\n`,
|
|
160
|
+
);
|
|
161
|
+
}
|
|
131
162
|
}
|
|
132
163
|
|
|
133
|
-
// content://N — unparsed text
|
|
164
|
+
// content://N — unparsed text. 400 Bad Request because anything in
|
|
165
|
+
// unparsed is text the parser couldn't dispatch (malformed XML, native
|
|
166
|
+
// tool call attempts, reasoning bleed). Visible to the model so it
|
|
167
|
+
// sees the rejection on its next turn and can correct.
|
|
134
168
|
if (unparsed) {
|
|
135
|
-
await store.
|
|
169
|
+
await store.set({
|
|
170
|
+
runId,
|
|
171
|
+
turn,
|
|
172
|
+
path: `content://${turn}`,
|
|
173
|
+
body: unparsed,
|
|
174
|
+
state: "failed",
|
|
175
|
+
outcome: "unparsed",
|
|
136
176
|
loopId,
|
|
137
|
-
|
|
177
|
+
visibility: "visible",
|
|
178
|
+
writer: "system",
|
|
138
179
|
});
|
|
139
180
|
}
|
|
140
181
|
|
|
141
|
-
// Commit usage stats
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
usage.
|
|
147
|
-
usage.
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
usage.
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
182
|
+
// Commit usage stats. Providers surface token counts under
|
|
183
|
+
// incompatible keys; walk them in priority order and fall back
|
|
184
|
+
// to 0 only as the definitional "not reported" value.
|
|
185
|
+
const usage = result.usage ? result.usage : {};
|
|
186
|
+
const cachedSources = [
|
|
187
|
+
usage.cached_tokens,
|
|
188
|
+
usage.prompt_tokens_details?.cached_tokens,
|
|
189
|
+
usage.input_tokens_details?.cached_tokens,
|
|
190
|
+
usage.cache_read_input_tokens,
|
|
191
|
+
];
|
|
192
|
+
const reasoningSources = [
|
|
193
|
+
usage.reasoning_tokens,
|
|
194
|
+
usage.completion_tokens_details?.reasoning_tokens,
|
|
195
|
+
usage.output_tokens_details?.reasoning_tokens,
|
|
196
|
+
];
|
|
197
|
+
let cachedTokens = 0;
|
|
198
|
+
for (const v of cachedSources)
|
|
199
|
+
if (v) {
|
|
200
|
+
cachedTokens = v;
|
|
201
|
+
break;
|
|
202
|
+
}
|
|
203
|
+
let reasoningTokens = 0;
|
|
204
|
+
for (const v of reasoningSources)
|
|
205
|
+
if (v) {
|
|
206
|
+
reasoningTokens = v;
|
|
207
|
+
break;
|
|
208
|
+
}
|
|
209
|
+
// Use LLM's actual prompt_tokens as the ground-truth context size
|
|
210
|
+
// when available; falls back to our pre-call estimate.
|
|
211
|
+
let actualContextTokens = 0;
|
|
212
|
+
if (usage.prompt_tokens) actualContextTokens = usage.prompt_tokens;
|
|
213
|
+
else if (assembledTokens) actualContextTokens = assembledTokens;
|
|
214
|
+
const numberOrZero = (v) => (typeof v === "number" ? v : 0);
|
|
215
|
+
await rummy.entries.updateTurnStats({
|
|
158
216
|
id: rummy.turnId,
|
|
159
217
|
context_tokens: actualContextTokens,
|
|
160
|
-
reasoning_content: responseMessage?.reasoning_content
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
218
|
+
reasoning_content: responseMessage?.reasoning_content
|
|
219
|
+
? responseMessage.reasoning_content
|
|
220
|
+
: null,
|
|
221
|
+
prompt_tokens: numberOrZero(usage.prompt_tokens),
|
|
222
|
+
cached_tokens: cachedTokens,
|
|
223
|
+
completion_tokens: numberOrZero(usage.completion_tokens),
|
|
224
|
+
reasoning_tokens: reasoningTokens,
|
|
225
|
+
total_tokens: numberOrZero(usage.total_tokens),
|
|
226
|
+
cost: numberOrZero(usage.cost),
|
|
167
227
|
});
|
|
168
228
|
}
|
|
169
229
|
|
|
170
230
|
async #logMessages(messages, context) {
|
|
231
|
+
this.#currentRunAlias = context.runAlias
|
|
232
|
+
? context.runAlias
|
|
233
|
+
: `run_${context.runId}`;
|
|
234
|
+
this.#currentTurn = context.turn === undefined ? null : context.turn;
|
|
235
|
+
const turnLabel = this.#currentTurn === null ? "?" : this.#currentTurn;
|
|
171
236
|
this.#turnLog.push(
|
|
172
|
-
`\n${"=".repeat(60)}\nTURN — model=${context.model} run=${
|
|
237
|
+
`\n${"=".repeat(60)}\nTURN ${turnLabel} — model=${context.model} run=${this.#currentRunAlias}\n${"=".repeat(60)}`,
|
|
173
238
|
);
|
|
174
239
|
for (const msg of messages) {
|
|
175
240
|
const label = msg.role.toUpperCase();
|
|
@@ -184,20 +249,29 @@ export default class Telemetry {
|
|
|
184
249
|
|
|
185
250
|
async #logResponse(response) {
|
|
186
251
|
const msg = response.choices?.[0]?.message;
|
|
187
|
-
|
|
252
|
+
const content = msg?.content ? msg.content : "(empty)";
|
|
253
|
+
this.#turnLog.push(`\n--- ASSISTANT ---\n${content}`);
|
|
188
254
|
if (msg?.reasoning_content) {
|
|
189
255
|
this.#turnLog.push(`\n--- REASONING ---\n${msg.reasoning_content}`);
|
|
190
256
|
}
|
|
191
|
-
const usage = response.usage
|
|
257
|
+
const usage = response.usage ? response.usage : {};
|
|
192
258
|
this.#turnLog.push(`\n--- USAGE ---\n${JSON.stringify(usage)}`);
|
|
193
259
|
this.#flush();
|
|
260
|
+
this.#writeTurnFile();
|
|
194
261
|
return response;
|
|
195
262
|
}
|
|
196
263
|
|
|
197
|
-
#flush() {
|
|
264
|
+
async #flush() {
|
|
198
265
|
if (!this.#lastRunPath || this.#turnLog.length === 0) return;
|
|
199
|
-
writeFile(this.#lastRunPath, `${this.#turnLog.join("\n")}\n`)
|
|
200
|
-
|
|
201
|
-
|
|
266
|
+
await writeFile(this.#lastRunPath, `${this.#turnLog.join("\n")}\n`);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
async #writeTurnFile() {
|
|
270
|
+
if (!this.#turnsDir || !this.#currentRunAlias || this.#currentTurn == null)
|
|
271
|
+
return;
|
|
272
|
+
const runDir = join(this.#turnsDir, this.#currentRunAlias);
|
|
273
|
+
await mkdir(runDir, { recursive: true });
|
|
274
|
+
const fileName = `turn_${String(this.#currentTurn).padStart(3, "0")}.txt`;
|
|
275
|
+
await writeFile(join(runDir, fileName), `${this.#turnLog.join("\n")}\n`);
|
|
202
276
|
}
|
|
203
277
|
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import docs from "./thinkDoc.js";
|
|
2
2
|
|
|
3
3
|
const THINK_ENABLED = process.env.RUMMY_THINK;
|
|
4
|
-
if (THINK_ENABLED === undefined)
|
|
4
|
+
if (THINK_ENABLED === undefined)
|
|
5
|
+
throw new Error("RUMMY_THINK must be set (1 or 0)");
|
|
5
6
|
|
|
6
7
|
export default class Think {
|
|
7
8
|
constructor(core) {
|
|
@@ -13,5 +14,17 @@ export default class Think {
|
|
|
13
14
|
return docsMap;
|
|
14
15
|
});
|
|
15
16
|
}
|
|
17
|
+
|
|
18
|
+
// Merge <think> tag bodies into the turn's reasoning_content so
|
|
19
|
+
// models without a dedicated reasoning channel still expose their
|
|
20
|
+
// reasoning through the same field.
|
|
21
|
+
core.filter("llm.reasoning", (reasoning, { commands }) => {
|
|
22
|
+
const thinkText = commands
|
|
23
|
+
.filter((c) => c.name === "think")
|
|
24
|
+
.map((c) => c.body)
|
|
25
|
+
.filter(Boolean)
|
|
26
|
+
.join("\n");
|
|
27
|
+
return [reasoning, thinkText].filter(Boolean).join("\n");
|
|
28
|
+
});
|
|
16
29
|
}
|
|
17
30
|
}
|
|
@@ -1,18 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
// Text goes to the model. Rationale stays in source.
|
|
3
|
-
// Changing ANY line requires reading ALL rationales first.
|
|
4
|
-
const LINES = [
|
|
5
|
-
[
|
|
6
|
-
"## <think>[reasoning]</think> - Think before acting",
|
|
7
|
-
],
|
|
8
|
-
[
|
|
9
|
-
"* Use <think> before any other tools to plan your approach",
|
|
10
|
-
"Positioning: think first, then act. Prevents degenerate tool-call storms.",
|
|
11
|
-
],
|
|
12
|
-
[
|
|
13
|
-
"* Reasoning inside <think> is private — it does not appear in your context",
|
|
14
|
-
"Frees the model to reason without consuming context budget.",
|
|
15
|
-
],
|
|
16
|
-
];
|
|
1
|
+
import { loadDoc } from "../helpers.js";
|
|
17
2
|
|
|
18
|
-
export default
|
|
3
|
+
export default loadDoc(import.meta.url, "thinkDoc.md");
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
## <think>[reasoning]</think> - Think before acting
|
|
2
|
+
|
|
3
|
+
* Use <think></think> before any other tools to plan your approach
|
|
4
|
+
<!-- Positioning: think first, then act. Prevents degenerate tool-call storms. -->
|
|
5
|
+
|
|
6
|
+
* Reasoning inside <think></think> is private — it does not appear in your context
|
|
7
|
+
<!-- Frees the model to reason without consuming context budget. -->
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# unknown
|
|
1
|
+
# unknown {#unknown_plugin}
|
|
2
2
|
|
|
3
3
|
The Rumsfeld mechanism. The model registers what it doesn't know before acting.
|
|
4
4
|
|
|
@@ -9,7 +9,7 @@ The Rumsfeld mechanism. The model registers what it doesn't know before acting.
|
|
|
9
9
|
- **Tool**: `unknown`
|
|
10
10
|
- **Category**: `unknown`
|
|
11
11
|
- **Handler**: None — recorded by TurnExecutor, deduplicated against existing unknowns.
|
|
12
|
-
- **Filter**: `assembly.
|
|
12
|
+
- **Filter**: `assembly.user` at priority 200 — renders `<unknowns>` adjacent to `<prompt>` (priority 300), after `<performed>` (priority 100). Unknowns are active work, not stable environment state; they belong in the user packet.
|
|
13
13
|
|
|
14
14
|
## Projection
|
|
15
15
|
|
|
@@ -20,5 +20,5 @@ The Rumsfeld mechanism. The model registers what it doesn't know before acting.
|
|
|
20
20
|
Unknowns are sticky — they persist across turns until the model explicitly
|
|
21
21
|
removes them with `<rm>`. The model investigates unknowns using `<get>`,
|
|
22
22
|
`<env>`, or `<ask_user>`, then removes resolved ones. Server deduplicates
|
|
23
|
-
on insert. Each unknown renders with turn,
|
|
23
|
+
on insert. Each unknown renders with turn, visibility, and tokens for
|
|
24
24
|
temporal reasoning and context management.
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import docs from "./unknownDoc.js";
|
|
2
|
-
|
|
3
1
|
export default class Unknown {
|
|
4
2
|
#core;
|
|
5
3
|
|
|
@@ -10,13 +8,10 @@ export default class Unknown {
|
|
|
10
8
|
category: "unknown",
|
|
11
9
|
});
|
|
12
10
|
core.on("handler", this.handler.bind(this));
|
|
13
|
-
core.on("
|
|
14
|
-
core.on("
|
|
15
|
-
core.filter("assembly.
|
|
16
|
-
core.
|
|
17
|
-
docsMap.unknown = docs;
|
|
18
|
-
return docsMap;
|
|
19
|
-
});
|
|
11
|
+
core.on("visible", this.full.bind(this));
|
|
12
|
+
core.on("summarized", this.summary.bind(this));
|
|
13
|
+
core.filter("assembly.user", this.assembleUnknowns.bind(this), 200);
|
|
14
|
+
core.markHidden();
|
|
20
15
|
}
|
|
21
16
|
|
|
22
17
|
async handler(entry, rummy) {
|
|
@@ -25,32 +20,72 @@ export default class Unknown {
|
|
|
25
20
|
// Deduplicate — if this exact body already exists, skip
|
|
26
21
|
const existingValues = await store.getUnknownValues(runId);
|
|
27
22
|
if (existingValues.has(entry.body)) {
|
|
28
|
-
|
|
23
|
+
await this.#core.hooks.error.log.emit({
|
|
24
|
+
store,
|
|
25
|
+
runId,
|
|
26
|
+
turn,
|
|
27
|
+
loopId,
|
|
28
|
+
message: `Unknown deduped: "${entry.body.slice(0, 60)}"`,
|
|
29
|
+
});
|
|
29
30
|
return;
|
|
30
31
|
}
|
|
31
32
|
|
|
32
|
-
// Generate slug path and upsert
|
|
33
|
-
|
|
34
|
-
await store.
|
|
33
|
+
// Generate slug path and upsert. Summary (if provided) becomes the
|
|
34
|
+
// path so the model can round-trip it via <get>; body is the fallback.
|
|
35
|
+
const unknownPath = await store.slugPath(
|
|
36
|
+
runId,
|
|
37
|
+
"unknown",
|
|
38
|
+
entry.body,
|
|
39
|
+
entry.attributes?.summary,
|
|
40
|
+
);
|
|
41
|
+
await store.set({
|
|
42
|
+
runId,
|
|
43
|
+
turn,
|
|
44
|
+
path: unknownPath,
|
|
45
|
+
body: entry.body,
|
|
46
|
+
state: "resolved",
|
|
47
|
+
loopId,
|
|
48
|
+
});
|
|
35
49
|
}
|
|
36
50
|
|
|
37
51
|
full(entry) {
|
|
38
|
-
return
|
|
52
|
+
return entry.body;
|
|
39
53
|
}
|
|
40
54
|
|
|
55
|
+
// Same principle as knowns: keep the first 500 characters on
|
|
56
|
+
// summarized unknowns so demotion doesn't erase the question,
|
|
57
|
+
// but cap large bodies to bound the packet cost.
|
|
41
58
|
summary(entry) {
|
|
42
|
-
|
|
59
|
+
if (!entry.body) return "";
|
|
60
|
+
if (entry.body.length <= 500) return entry.body;
|
|
61
|
+
return `${entry.body.slice(0, 500)}\n[truncated — promote to see the full question]`;
|
|
43
62
|
}
|
|
44
63
|
|
|
45
64
|
async assembleUnknowns(content, ctx) {
|
|
46
65
|
const entries = ctx.rows.filter((r) => r.category === "unknown");
|
|
47
66
|
if (entries.length === 0) return content;
|
|
67
|
+
const lines = entries.map((e) => renderUnknownTag(e));
|
|
68
|
+
return `${content}<unknowns>\n${lines.join("\n")}\n</unknowns>\n`;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
48
71
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
72
|
+
function renderUnknownTag(entry) {
|
|
73
|
+
const attrs =
|
|
74
|
+
typeof entry.attributes === "string"
|
|
75
|
+
? JSON.parse(entry.attributes)
|
|
76
|
+
: entry.attributes;
|
|
77
|
+
const turn = entry.source_turn ? ` turn="${entry.source_turn}"` : "";
|
|
78
|
+
const visibility = entry.visibility
|
|
79
|
+
? ` visibility="${entry.visibility}"`
|
|
80
|
+
: "";
|
|
81
|
+
const tokens = entry.aTokens != null ? ` tokens="${entry.aTokens}"` : "";
|
|
82
|
+
const summary =
|
|
83
|
+
typeof attrs?.summary === "string"
|
|
84
|
+
? ` summary="${attrs.summary.replace(/"/g, "'").slice(0, 80)}"`
|
|
85
|
+
: "";
|
|
86
|
+
const attrStr = `${turn}${summary}${visibility}${tokens}`;
|
|
87
|
+
if (entry.body) {
|
|
88
|
+
return `<unknown path="${entry.path}"${attrStr}>${entry.body}</unknown>`;
|
|
55
89
|
}
|
|
90
|
+
return `<unknown path="${entry.path}"${attrStr}/>`;
|
|
56
91
|
}
|
|
@@ -1,26 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
// Text goes to the model. Rationale stays in source.
|
|
3
|
-
// Changing ANY line requires reading ALL rationales first.
|
|
4
|
-
const LINES = [
|
|
5
|
-
[
|
|
6
|
-
"## <unknown>[specific thing I need to learn]</unknown> - Track open questions",
|
|
7
|
-
],
|
|
8
|
-
[
|
|
9
|
-
'Example: <unknown path="unknown://answer">contents of answer.txt</unknown>',
|
|
10
|
-
"Path form: explicit unknown path for structured tracking.",
|
|
11
|
-
],
|
|
12
|
-
[
|
|
13
|
-
"Example: <unknown>which database adapter is configured</unknown>",
|
|
14
|
-
"Body form: question as body, path auto-generated.",
|
|
15
|
-
],
|
|
16
|
-
[
|
|
17
|
-
"* Investigate with Tool Commands",
|
|
18
|
-
"Unknowns drive action — get, env, search, ask_user.",
|
|
19
|
-
],
|
|
20
|
-
[
|
|
21
|
-
'* When resolved or irrelevant, remove with <set path="unknown://..." fidelity="archive"/>',
|
|
22
|
-
"Archive instead of delete — preserves the question for context history.",
|
|
23
|
-
],
|
|
24
|
-
];
|
|
1
|
+
import { loadDoc } from "../helpers.js";
|
|
25
2
|
|
|
26
|
-
export default
|
|
3
|
+
export default loadDoc(import.meta.url, "unknownDoc.md");
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
## <set path="unknown://{question}">[specific thing I need to learn]</set> - Register gaps for research
|
|
2
|
+
<!-- Use <set> to write unknown entries (not <unknown>). Matches instructions examples. -->
|
|
3
|
+
|
|
4
|
+
Example: <set path="unknown://answer" summary="answer,contents">contents of answer.txt</set>
|
|
5
|
+
<!-- Path form: explicit unknown path for structured tracking. -->
|
|
6
|
+
|
|
7
|
+
* Investigate with Tool Commands
|
|
8
|
+
<!-- Unknowns drive action — get, env, search, ask_user. -->
|
|
9
|
+
|
|
10
|
+
* When resolved or irrelevant, remove with <set path="unknown://..." visibility="archived"/>
|
|
11
|
+
<!-- Archive instead of delete — preserves the question for context history. -->
|
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
import docs from "./updateDoc.js";
|
|
2
2
|
|
|
3
|
+
const TERMINAL_STATUSES = new Set([200, 204, 422, 500]);
|
|
4
|
+
|
|
5
|
+
const CONTRACT_REMINDER = "Missing update";
|
|
6
|
+
|
|
7
|
+
const EMPTY_RESPONSE_REMINDER =
|
|
8
|
+
"Response empty - Update with status 500 if unable to fulfill request.";
|
|
9
|
+
|
|
10
|
+
function isValidStatus(status) {
|
|
11
|
+
if (TERMINAL_STATUSES.has(status)) return true;
|
|
12
|
+
return Number.isInteger(status) && status >= 100 && status < 200;
|
|
13
|
+
}
|
|
14
|
+
|
|
3
15
|
export default class Update {
|
|
4
16
|
#core;
|
|
5
17
|
|
|
@@ -8,18 +20,68 @@ export default class Update {
|
|
|
8
20
|
core.ensureTool();
|
|
9
21
|
core.registerScheme({ category: "logging" });
|
|
10
22
|
core.on("handler", this.handler.bind(this));
|
|
11
|
-
core.on("
|
|
12
|
-
core.on("
|
|
23
|
+
core.on("visible", this.full.bind(this));
|
|
24
|
+
core.on("summarized", this.summary.bind(this));
|
|
13
25
|
core.filter("instructions.toolDocs", async (docsMap) => {
|
|
14
26
|
docsMap.update = docs;
|
|
15
27
|
return docsMap;
|
|
16
28
|
});
|
|
29
|
+
core.hooks.update = {
|
|
30
|
+
resolve: this.resolve.bind(this),
|
|
31
|
+
};
|
|
17
32
|
}
|
|
18
33
|
|
|
19
34
|
async handler(entry, rummy) {
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
35
|
+
const status = entry.attributes?.status ?? 102;
|
|
36
|
+
await rummy.update(entry.body, { status });
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Classify this turn's update state.
|
|
41
|
+
*
|
|
42
|
+
* Returns { summaryText, updateText }:
|
|
43
|
+
* - summaryText: non-null → model claimed terminal (200/204/422)
|
|
44
|
+
* - updateText: non-null → model is continuing (1xx)
|
|
45
|
+
*
|
|
46
|
+
* Errors (invalid status, missing update) emit via hooks.error.log.
|
|
47
|
+
* The "terminal + turn had errors → not actually terminal" rule
|
|
48
|
+
* lives in the error plugin's verdict, not here.
|
|
49
|
+
*/
|
|
50
|
+
async resolve({ recorded, content, runId, turn, loopId, rummy }) {
|
|
51
|
+
const entry = recorded.findLast((e) => e.scheme === "update");
|
|
52
|
+
const status = entry?.attributes?.status ?? 102;
|
|
53
|
+
const isTerminal = TERMINAL_STATUSES.has(status);
|
|
54
|
+
let summaryText = null;
|
|
55
|
+
let updateText = null;
|
|
56
|
+
if (entry?.body) {
|
|
57
|
+
if (isTerminal) summaryText = entry.body;
|
|
58
|
+
else updateText = entry.body;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (entry && !isValidStatus(status)) {
|
|
62
|
+
await rummy.hooks.error.log.emit({
|
|
63
|
+
store: rummy.entries,
|
|
64
|
+
runId,
|
|
65
|
+
turn,
|
|
66
|
+
loopId,
|
|
67
|
+
message: `Invalid status ${entry.attributes?.status} on update — use 1xx to continue or 200 to conclude.`,
|
|
68
|
+
status: 422,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (!summaryText && !updateText) {
|
|
73
|
+
const empty = !content || content.trim() === "";
|
|
74
|
+
await rummy.hooks.error.log.emit({
|
|
75
|
+
store: rummy.entries,
|
|
76
|
+
runId,
|
|
77
|
+
turn,
|
|
78
|
+
loopId,
|
|
79
|
+
message: empty ? EMPTY_RESPONSE_REMINDER : CONTRACT_REMINDER,
|
|
80
|
+
status: 422,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return { summaryText, updateText };
|
|
23
85
|
}
|
|
24
86
|
|
|
25
87
|
full(entry) {
|