@possumtech/rummy 0.5.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 -5
- 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 -330
- package/src/agent/ContextAssembler.js +4 -4
- package/src/agent/Entries.js +655 -0
- package/src/agent/ProjectAgent.js +30 -18
- package/src/agent/TurnExecutor.js +229 -421
- package/src/agent/XmlParser.js +99 -33
- 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 -125
- 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 +29 -21
- package/src/{server → hooks}/RpcRegistry.js +2 -1
- package/src/hooks/RummyContext.js +135 -35
- package/src/hooks/ToolRegistry.js +21 -16
- 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 -25
- package/src/plugins/ask_user/ask_userDoc.md +10 -0
- package/src/plugins/budget/README.md +27 -25
- package/src/plugins/budget/budget.js +260 -88
- package/src/plugins/cp/README.md +2 -2
- package/src/plugins/cp/cp.js +29 -11
- package/src/plugins/cp/cpDoc.js +2 -15
- 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 +45 -6
- 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 -2
- package/src/plugins/get/README.md +1 -1
- package/src/plugins/get/get.js +103 -48
- package/src/plugins/get/getDoc.js +2 -32
- 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 +42 -2
- package/src/plugins/index.js +146 -123
- package/src/plugins/instructions/README.md +35 -9
- package/src/plugins/instructions/instructions.js +122 -9
- 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 +67 -36
- package/src/plugins/known/knownDoc.js +2 -17
- 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 +55 -22
- package/src/plugins/mv/mvDoc.js +2 -18
- 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 +58 -16
- package/src/plugins/rm/README.md +1 -1
- package/src/plugins/rm/rm.js +56 -12
- package/src/plugins/rm/rmDoc.js +2 -20
- 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 -75
- 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 +50 -6
- 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 -18
- 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 +129 -80
- package/src/plugins/think/README.md +1 -1
- package/src/plugins/think/think.js +12 -0
- package/src/plugins/think/thinkDoc.js +2 -15
- package/src/plugins/think/thinkDoc.md +7 -0
- package/src/plugins/unknown/README.md +3 -3
- package/src/plugins/unknown/unknown.js +47 -19
- package/src/plugins/unknown/unknownDoc.js +2 -21
- 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 -30
- 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/v_model_context.sql +27 -31
- package/src/sql/v_run_log.sql +9 -14
- package/EXCEPTIONS.md +0 -46
- package/FIDELITY_CONTRACT.md +0 -172
- package/src/agent/KnownStore.js +0 -337
- package/src/agent/ResponseHealer.js +0 -241
- 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 -45
- 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 -56
- package/src/plugins/progress/README.md +0 -16
- package/src/plugins/progress/progress.js +0 -43
- package/src/plugins/summarize/README.md +0 -19
- package/src/plugins/summarize/summarize.js +0 -32
- package/src/plugins/summarize/summarizeDoc.js +0 -27
|
@@ -30,14 +30,15 @@ export default class Telemetry {
|
|
|
30
30
|
|
|
31
31
|
async #onRpcStarted({ method, id, params }) {
|
|
32
32
|
this.#starts.set(id, Date.now());
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
+
}
|
|
41
42
|
console.log(`[RPC] → ${method}(${id})${summary ? ` ${summary}` : ""}`);
|
|
42
43
|
|
|
43
44
|
if (method === "ask" || method === "act") {
|
|
@@ -50,11 +51,13 @@ export default class Telemetry {
|
|
|
50
51
|
? `${((Date.now() - this.#starts.get(id)) / 1000).toFixed(1)}s`
|
|
51
52
|
: "";
|
|
52
53
|
this.#starts.delete(id);
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
+
}
|
|
58
61
|
console.log(
|
|
59
62
|
`[RPC] ← ${method}(${id}) ${elapsed}${summary ? ` ${summary}` : ""}`,
|
|
60
63
|
);
|
|
@@ -65,7 +68,8 @@ export default class Telemetry {
|
|
|
65
68
|
? `${((Date.now() - this.#starts.get(id)) / 1000).toFixed(1)}s`
|
|
66
69
|
: "";
|
|
67
70
|
this.#starts.delete(id);
|
|
68
|
-
|
|
71
|
+
const detail = error?.message ? error.message : error;
|
|
72
|
+
console.error(`[RPC] ✗ (${id}) ${elapsed} ${detail}`);
|
|
69
73
|
}
|
|
70
74
|
|
|
71
75
|
async #onStepCompleted(payload) {
|
|
@@ -87,53 +91,74 @@ export default class Telemetry {
|
|
|
87
91
|
userMsg,
|
|
88
92
|
}) {
|
|
89
93
|
const { entries: store, runId, loopId } = rummy;
|
|
94
|
+
// Audit schemes are system-only writes (see initPlugins).
|
|
95
|
+
const systemOpts = { loopId, visibility: "archived", writer: "system" };
|
|
90
96
|
|
|
91
97
|
// assistant://N — the model's raw response
|
|
92
|
-
await store.
|
|
93
|
-
|
|
94
|
-
|
|
98
|
+
await store.set({
|
|
99
|
+
runId,
|
|
100
|
+
turn,
|
|
101
|
+
path: `assistant://${turn}`,
|
|
102
|
+
body: content,
|
|
103
|
+
state: "resolved",
|
|
104
|
+
...systemOpts,
|
|
95
105
|
});
|
|
96
106
|
|
|
97
107
|
// system://N, user://N — assembled messages as audit
|
|
98
108
|
if (systemMsg) {
|
|
99
|
-
await store.
|
|
100
|
-
|
|
101
|
-
|
|
109
|
+
await store.set({
|
|
110
|
+
runId,
|
|
111
|
+
turn,
|
|
112
|
+
path: `system://${turn}`,
|
|
113
|
+
body: systemMsg,
|
|
114
|
+
state: "resolved",
|
|
115
|
+
...systemOpts,
|
|
102
116
|
});
|
|
103
117
|
}
|
|
104
118
|
if (userMsg) {
|
|
105
|
-
await store.
|
|
106
|
-
|
|
107
|
-
|
|
119
|
+
await store.set({
|
|
120
|
+
runId,
|
|
121
|
+
turn,
|
|
122
|
+
path: `user://${turn}`,
|
|
123
|
+
body: userMsg,
|
|
124
|
+
state: "resolved",
|
|
125
|
+
...systemOpts,
|
|
108
126
|
});
|
|
109
127
|
}
|
|
110
128
|
|
|
111
129
|
// model://N — raw API response diagnostics
|
|
112
|
-
await store.
|
|
130
|
+
await store.set({
|
|
113
131
|
runId,
|
|
114
132
|
turn,
|
|
115
|
-
`model://${turn}`,
|
|
116
|
-
JSON.stringify({
|
|
133
|
+
path: `model://${turn}`,
|
|
134
|
+
body: JSON.stringify({
|
|
117
135
|
keys: responseMessage ? Object.keys(responseMessage) : [],
|
|
118
|
-
reasoning_content: responseMessage?.reasoning_content
|
|
136
|
+
reasoning_content: responseMessage?.reasoning_content
|
|
137
|
+
? responseMessage.reasoning_content
|
|
138
|
+
: null,
|
|
119
139
|
content: content.slice(0, 4096),
|
|
120
|
-
usage: result.usage
|
|
121
|
-
model: result.model
|
|
140
|
+
usage: result.usage ? result.usage : null,
|
|
141
|
+
model: result.model ? result.model : null,
|
|
122
142
|
}),
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
);
|
|
143
|
+
state: "resolved",
|
|
144
|
+
...systemOpts,
|
|
145
|
+
});
|
|
126
146
|
|
|
127
147
|
// reasoning://N
|
|
128
148
|
if (responseMessage?.reasoning_content) {
|
|
129
|
-
await store.
|
|
149
|
+
await store.set({
|
|
130
150
|
runId,
|
|
131
151
|
turn,
|
|
132
|
-
`reasoning://${turn}`,
|
|
133
|
-
responseMessage.reasoning_content,
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
);
|
|
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
|
+
}
|
|
137
162
|
}
|
|
138
163
|
|
|
139
164
|
// content://N — unparsed text. 400 Bad Request because anything in
|
|
@@ -141,46 +166,75 @@ export default class Telemetry {
|
|
|
141
166
|
// tool call attempts, reasoning bleed). Visible to the model so it
|
|
142
167
|
// sees the rejection on its next turn and can correct.
|
|
143
168
|
if (unparsed) {
|
|
144
|
-
await store.
|
|
169
|
+
await store.set({
|
|
170
|
+
runId,
|
|
171
|
+
turn,
|
|
172
|
+
path: `content://${turn}`,
|
|
173
|
+
body: unparsed,
|
|
174
|
+
state: "failed",
|
|
175
|
+
outcome: "unparsed",
|
|
145
176
|
loopId,
|
|
146
|
-
|
|
177
|
+
visibility: "visible",
|
|
178
|
+
writer: "system",
|
|
147
179
|
});
|
|
148
180
|
}
|
|
149
181
|
|
|
150
|
-
// Commit usage stats
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
usage.
|
|
156
|
-
usage.
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
usage.
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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({
|
|
167
216
|
id: rummy.turnId,
|
|
168
217
|
context_tokens: actualContextTokens,
|
|
169
|
-
reasoning_content: responseMessage?.reasoning_content
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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),
|
|
176
227
|
});
|
|
177
228
|
}
|
|
178
229
|
|
|
179
230
|
async #logMessages(messages, context) {
|
|
180
|
-
this.#currentRunAlias = context.runAlias
|
|
181
|
-
|
|
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;
|
|
182
236
|
this.#turnLog.push(
|
|
183
|
-
`\n${"=".repeat(60)}\nTURN ${
|
|
237
|
+
`\n${"=".repeat(60)}\nTURN ${turnLabel} — model=${context.model} run=${this.#currentRunAlias}\n${"=".repeat(60)}`,
|
|
184
238
|
);
|
|
185
239
|
for (const msg of messages) {
|
|
186
240
|
const label = msg.role.toUpperCase();
|
|
@@ -195,34 +249,29 @@ export default class Telemetry {
|
|
|
195
249
|
|
|
196
250
|
async #logResponse(response) {
|
|
197
251
|
const msg = response.choices?.[0]?.message;
|
|
198
|
-
|
|
252
|
+
const content = msg?.content ? msg.content : "(empty)";
|
|
253
|
+
this.#turnLog.push(`\n--- ASSISTANT ---\n${content}`);
|
|
199
254
|
if (msg?.reasoning_content) {
|
|
200
255
|
this.#turnLog.push(`\n--- REASONING ---\n${msg.reasoning_content}`);
|
|
201
256
|
}
|
|
202
|
-
const usage = response.usage
|
|
257
|
+
const usage = response.usage ? response.usage : {};
|
|
203
258
|
this.#turnLog.push(`\n--- USAGE ---\n${JSON.stringify(usage)}`);
|
|
204
259
|
this.#flush();
|
|
205
260
|
this.#writeTurnFile();
|
|
206
261
|
return response;
|
|
207
262
|
}
|
|
208
263
|
|
|
209
|
-
#flush() {
|
|
264
|
+
async #flush() {
|
|
210
265
|
if (!this.#lastRunPath || this.#turnLog.length === 0) return;
|
|
211
|
-
writeFile(this.#lastRunPath, `${this.#turnLog.join("\n")}\n`)
|
|
212
|
-
() => {},
|
|
213
|
-
);
|
|
266
|
+
await writeFile(this.#lastRunPath, `${this.#turnLog.join("\n")}\n`);
|
|
214
267
|
}
|
|
215
268
|
|
|
216
269
|
async #writeTurnFile() {
|
|
217
270
|
if (!this.#turnsDir || !this.#currentRunAlias || this.#currentTurn == null)
|
|
218
271
|
return;
|
|
219
272
|
const runDir = join(this.#turnsDir, this.#currentRunAlias);
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
await writeFile(join(runDir, fileName), `${this.#turnLog.join("\n")}\n`);
|
|
224
|
-
} catch {
|
|
225
|
-
// best effort — diagnostic feature, don't fail the turn
|
|
226
|
-
}
|
|
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`);
|
|
227
276
|
}
|
|
228
277
|
}
|
|
@@ -14,5 +14,17 @@ export default class Think {
|
|
|
14
14
|
return docsMap;
|
|
15
15
|
});
|
|
16
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
|
+
});
|
|
17
29
|
}
|
|
18
30
|
}
|
|
@@ -1,16 +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
|
-
["## <think>[reasoning]</think> - Think before acting"],
|
|
6
|
-
[
|
|
7
|
-
"* Use <think></think> before any other tools to plan your approach",
|
|
8
|
-
"Positioning: think first, then act. Prevents degenerate tool-call storms.",
|
|
9
|
-
],
|
|
10
|
-
[
|
|
11
|
-
"* Reasoning inside <think></think> is private — it does not appear in your context",
|
|
12
|
-
"Frees the model to reason without consuming context budget.",
|
|
13
|
-
],
|
|
14
|
-
];
|
|
1
|
+
import { loadDoc } from "../helpers.js";
|
|
15
2
|
|
|
16
|
-
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.
|
|
@@ -8,12 +8,9 @@ export default class Unknown {
|
|
|
8
8
|
category: "unknown",
|
|
9
9
|
});
|
|
10
10
|
core.on("handler", this.handler.bind(this));
|
|
11
|
-
core.on("
|
|
12
|
-
core.on("
|
|
13
|
-
core.filter("assembly.
|
|
14
|
-
// <unknown> is internal — written via <set path="unknown://...">. Hidden
|
|
15
|
-
// from all model-facing tool lists. Handler still dispatches if the
|
|
16
|
-
// model emits <unknown> directly out of habit.
|
|
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);
|
|
17
14
|
core.markHidden();
|
|
18
15
|
}
|
|
19
16
|
|
|
@@ -23,7 +20,13 @@ export default class Unknown {
|
|
|
23
20
|
// Deduplicate — if this exact body already exists, skip
|
|
24
21
|
const existingValues = await store.getUnknownValues(runId);
|
|
25
22
|
if (existingValues.has(entry.body)) {
|
|
26
|
-
|
|
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
|
+
});
|
|
27
30
|
return;
|
|
28
31
|
}
|
|
29
32
|
|
|
@@ -35,29 +38,54 @@ export default class Unknown {
|
|
|
35
38
|
entry.body,
|
|
36
39
|
entry.attributes?.summary,
|
|
37
40
|
);
|
|
38
|
-
await store.
|
|
41
|
+
await store.set({
|
|
42
|
+
runId,
|
|
43
|
+
turn,
|
|
44
|
+
path: unknownPath,
|
|
45
|
+
body: entry.body,
|
|
46
|
+
state: "resolved",
|
|
47
|
+
loopId,
|
|
48
|
+
});
|
|
39
49
|
}
|
|
40
50
|
|
|
41
51
|
full(entry) {
|
|
42
52
|
return entry.body;
|
|
43
53
|
}
|
|
44
54
|
|
|
45
|
-
|
|
46
|
-
|
|
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.
|
|
58
|
+
summary(entry) {
|
|
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]`;
|
|
47
62
|
}
|
|
48
63
|
|
|
49
64
|
async assembleUnknowns(content, ctx) {
|
|
50
65
|
const entries = ctx.rows.filter((r) => r.category === "unknown");
|
|
51
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
|
+
}
|
|
52
71
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
}
|
|
61
|
-
|
|
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>`;
|
|
62
89
|
}
|
|
90
|
+
return `<unknown path="${entry.path}"${attrStr}/>`;
|
|
63
91
|
}
|
|
@@ -1,22 +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> - Register gaps for research",
|
|
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
|
-
"* Investigate with Tool Commands",
|
|
14
|
-
"Unknowns drive action — get, env, search, ask_user.",
|
|
15
|
-
],
|
|
16
|
-
[
|
|
17
|
-
'* When resolved or irrelevant, remove with <set path="unknown://..." fidelity="archived"/>',
|
|
18
|
-
"Archive instead of delete — preserves the question for context history.",
|
|
19
|
-
],
|
|
20
|
-
];
|
|
1
|
+
import { loadDoc } from "../helpers.js";
|
|
21
2
|
|
|
22
|
-
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) {
|
|
@@ -1,31 +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
|
-
"## <update>[brief status]</update> - Heartbeat for ongoing work (one per turn, at the end)",
|
|
7
|
-
"Header defines position and frequency. Without this, model uses update as inline narration between tools — multiple updates per turn.",
|
|
8
|
-
],
|
|
9
|
-
[
|
|
10
|
-
"Example: <update>Reading config files</update>",
|
|
11
|
-
"Progress checkpoint. Status signal, not a log entry.",
|
|
12
|
-
],
|
|
13
|
-
[
|
|
14
|
-
"Example: <update>Found 3 issues, fixing first</update>",
|
|
15
|
-
"Multi-step progress. Ongoing work.",
|
|
16
|
-
],
|
|
17
|
-
[
|
|
18
|
-
"* Urgent: ONE <update></update> per turn, AT THE END. Not inline narration between tools.",
|
|
19
|
-
"Single-update-per-turn is the missing rule. Model was emitting 3-6 updates per turn as progress commentary.",
|
|
20
|
-
],
|
|
21
|
-
[
|
|
22
|
-
"* If you'd repeat the same <update></update> as last turn, the work is either stuck or done. Take a different action or <summarize></summarize>.",
|
|
23
|
-
"Points at the zombie-loop failure mode directly. Gives the model a trigger (same-text-as-prior-update) and two remedies.",
|
|
24
|
-
],
|
|
25
|
-
[
|
|
26
|
-
"* YOU MUST keep <update></update> to <= 80 characters",
|
|
27
|
-
"Length cap.",
|
|
28
|
-
],
|
|
29
|
-
];
|
|
1
|
+
import { loadDoc } from "../helpers.js";
|
|
30
2
|
|
|
31
|
-
export default
|
|
3
|
+
export default loadDoc(import.meta.url, "updateDoc.md");
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
## <update status="N">{brief status}</update> - Status report (exactly one per turn, at the end)
|
|
2
|
+
<!-- Header defines position, frequency, and status code requirement. -->
|
|
3
|
+
|
|
4
|
+
REQUIRED: the valid values of N are defined by your current stage instructions.
|
|
5
|
+
<!-- Single source of truth for codes is the current phase instructions block, not this doc. Listing codes here leaks termination knowledge (e.g. 200) that strong models use to short-circuit the protocol. -->
|
|
6
|
+
|
|
7
|
+
REQUIRED: YOU MUST keep <update></update> body to <= 80 characters.
|
|
8
|
+
<!-- Length cap. -->
|