@possumtech/rummy 0.2.7 → 0.3.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 +12 -3
- package/EXCEPTIONS.md +46 -0
- package/PLUGINS.md +454 -197
- package/SPEC.md +284 -93
- package/migrations/001_initial_schema.sql +57 -70
- package/package.json +16 -10
- package/service.js +1 -1
- package/src/agent/AgentLoop.js +254 -70
- package/src/agent/ContextAssembler.js +18 -4
- package/src/agent/KnownStore.js +156 -23
- package/src/agent/ProjectAgent.js +5 -4
- package/src/agent/ResponseHealer.js +21 -1
- package/src/agent/TurnExecutor.js +393 -115
- package/src/agent/XmlParser.js +92 -39
- package/src/agent/known_checks.sql +5 -4
- package/src/agent/known_queries.sql +4 -3
- package/src/agent/known_store.sql +45 -15
- package/src/agent/loops.sql +63 -0
- package/src/agent/runs.sql +7 -7
- package/src/agent/schemes.sql +5 -2
- package/src/agent/tokens.js +6 -21
- package/src/agent/turns.sql +13 -4
- package/src/hooks/Hooks.js +18 -0
- package/src/hooks/PluginContext.js +14 -10
- package/src/hooks/RummyContext.js +30 -10
- package/src/hooks/ToolRegistry.js +83 -19
- package/src/llm/LlmProvider.js +27 -8
- package/src/llm/OpenAiClient.js +20 -0
- package/src/llm/OpenRouterClient.js +24 -2
- package/src/llm/XaiClient.js +47 -2
- package/src/plugins/ask_user/README.md +4 -4
- package/src/plugins/ask_user/ask_user.js +8 -7
- package/src/plugins/ask_user/ask_userDoc.js +29 -0
- package/src/plugins/budget/BudgetGuard.js +74 -0
- package/src/plugins/budget/README.md +43 -0
- package/src/plugins/budget/budget.js +79 -0
- package/src/plugins/cp/README.md +5 -4
- package/src/plugins/cp/cp.js +16 -12
- package/src/plugins/cp/cpDoc.js +29 -0
- package/src/plugins/current/README.md +4 -4
- package/src/plugins/current/current.js +12 -10
- package/src/plugins/engine/engine.sql +5 -10
- package/src/plugins/engine/turn_context.sql +13 -13
- package/src/plugins/env/README.md +3 -4
- package/src/plugins/env/env.js +8 -7
- package/src/plugins/env/envDoc.js +29 -0
- package/src/plugins/file/README.md +9 -12
- package/src/plugins/file/file.js +34 -45
- package/src/plugins/get/README.md +2 -2
- package/src/plugins/get/get.js +28 -11
- package/src/plugins/get/getDoc.js +41 -0
- package/src/plugins/hedberg/docs.md +0 -9
- package/src/plugins/hedberg/hedberg.js +4 -6
- package/src/plugins/hedberg/matcher.js +1 -1
- package/src/plugins/hedberg/normalize.js +28 -0
- package/src/plugins/hedberg/patterns.js +31 -33
- package/src/plugins/hedberg/sed.js +17 -10
- package/src/plugins/helpers.js +2 -2
- package/src/plugins/index.js +93 -28
- package/src/plugins/instructions/README.md +6 -2
- package/src/plugins/instructions/instructions.js +21 -5
- package/src/plugins/instructions/preamble.md +9 -5
- package/src/plugins/known/README.md +10 -7
- package/src/plugins/known/known.js +33 -23
- package/src/plugins/known/knownDoc.js +33 -0
- package/src/plugins/mv/README.md +5 -4
- package/src/plugins/mv/mv.js +16 -12
- package/src/plugins/mv/mvDoc.js +31 -0
- package/src/plugins/persona/persona.js +78 -0
- package/src/plugins/previous/README.md +2 -2
- package/src/plugins/previous/previous.js +12 -8
- package/src/plugins/progress/progress.js +44 -12
- package/src/plugins/prompt/README.md +5 -5
- package/src/plugins/prompt/prompt.js +23 -19
- package/src/plugins/rm/README.md +4 -4
- package/src/plugins/rm/rm.js +29 -12
- package/src/plugins/rm/rmDoc.js +30 -0
- package/src/plugins/rpc/README.md +15 -28
- package/src/plugins/rpc/rpc.js +63 -107
- package/src/plugins/set/README.md +13 -12
- package/src/plugins/set/set.js +82 -21
- package/src/plugins/set/setDoc.js +45 -0
- package/src/plugins/sh/README.md +4 -4
- package/src/plugins/sh/sh.js +8 -7
- package/src/plugins/sh/shDoc.js +29 -0
- package/src/plugins/{skills/skills.js → skill/skill.js} +12 -54
- package/src/plugins/summarize/README.md +6 -5
- package/src/plugins/summarize/summarize.js +7 -6
- package/src/plugins/summarize/summarizeDoc.js +33 -0
- package/src/plugins/telemetry/telemetry.js +20 -8
- package/src/plugins/think/README.md +20 -0
- package/src/plugins/think/think.js +5 -0
- package/src/plugins/unknown/README.md +5 -5
- package/src/plugins/unknown/unknown.js +11 -8
- package/src/plugins/unknown/unknownDoc.js +31 -0
- package/src/plugins/update/README.md +3 -8
- package/src/plugins/update/update.js +7 -6
- package/src/plugins/update/updateDoc.js +33 -0
- package/src/server/ClientConnection.js +3 -5
- package/src/server/RpcRegistry.js +52 -4
- package/src/sql/v_model_context.sql +31 -39
- package/src/sql/v_run_log.sql +3 -3
- package/src/agent/prompt_queue.sql +0 -39
- package/src/plugins/ask_user/docs.md +0 -2
- package/src/plugins/cp/docs.md +0 -2
- package/src/plugins/env/docs.md +0 -2
- package/src/plugins/get/docs.md +0 -6
- package/src/plugins/known/docs.md +0 -3
- package/src/plugins/mv/docs.md +0 -2
- package/src/plugins/rm/docs.md +0 -4
- package/src/plugins/set/docs.md +0 -4
- package/src/plugins/sh/docs.md +0 -2
- package/src/plugins/skills/README.md +0 -25
- package/src/plugins/store/README.md +0 -20
- package/src/plugins/store/docs.md +0 -5
- package/src/plugins/store/store.js +0 -52
- package/src/plugins/summarize/docs.md +0 -4
- package/src/plugins/unknown/docs.md +0 -5
- package/src/plugins/update/docs.md +0 -4
|
@@ -25,9 +25,11 @@ export default class TurnExecutor {
|
|
|
25
25
|
projectId,
|
|
26
26
|
currentRunId,
|
|
27
27
|
currentAlias,
|
|
28
|
+
currentLoopId,
|
|
28
29
|
requestedModel,
|
|
29
30
|
loopPrompt,
|
|
30
|
-
|
|
31
|
+
noRepo,
|
|
32
|
+
toolSet,
|
|
31
33
|
contextSize,
|
|
32
34
|
options,
|
|
33
35
|
signal,
|
|
@@ -36,6 +38,7 @@ export default class TurnExecutor {
|
|
|
36
38
|
|
|
37
39
|
const turnRow = await this.#db.create_turn.get({
|
|
38
40
|
run_id: currentRunId,
|
|
41
|
+
loop_id: currentLoopId,
|
|
39
42
|
sequence: turn,
|
|
40
43
|
});
|
|
41
44
|
|
|
@@ -67,8 +70,10 @@ export default class TurnExecutor {
|
|
|
67
70
|
type: mode,
|
|
68
71
|
sequence: turn,
|
|
69
72
|
runId: currentRunId,
|
|
73
|
+
loopId: currentLoopId,
|
|
70
74
|
turnId: turnRow.id,
|
|
71
|
-
|
|
75
|
+
noRepo,
|
|
76
|
+
toolSet,
|
|
72
77
|
contextSize,
|
|
73
78
|
systemPrompt: null,
|
|
74
79
|
loopPrompt,
|
|
@@ -123,11 +128,12 @@ export default class TurnExecutor {
|
|
|
123
128
|
|
|
124
129
|
await this.#db.insert_turn_context.run({
|
|
125
130
|
run_id: currentRunId,
|
|
131
|
+
loop_id: currentLoopId,
|
|
126
132
|
turn,
|
|
127
133
|
ordinal: row.ordinal,
|
|
128
134
|
path: row.path,
|
|
129
135
|
fidelity: row.fidelity,
|
|
130
|
-
|
|
136
|
+
status: row.status,
|
|
131
137
|
body: projectedBody ?? "",
|
|
132
138
|
tokens: countTokens(projectedBody ?? ""),
|
|
133
139
|
attributes: row.attributes,
|
|
@@ -136,6 +142,14 @@ export default class TurnExecutor {
|
|
|
136
142
|
});
|
|
137
143
|
}
|
|
138
144
|
|
|
145
|
+
const demoted = [];
|
|
146
|
+
|
|
147
|
+
await this.#hooks.context.materialized.emit({
|
|
148
|
+
runId: currentRunId,
|
|
149
|
+
turn,
|
|
150
|
+
rowCount: viewRows.length,
|
|
151
|
+
});
|
|
152
|
+
|
|
139
153
|
await this.#hooks.run.progress.emit({
|
|
140
154
|
projectId,
|
|
141
155
|
run: currentAlias,
|
|
@@ -143,35 +157,86 @@ export default class TurnExecutor {
|
|
|
143
157
|
status: "thinking",
|
|
144
158
|
});
|
|
145
159
|
|
|
146
|
-
|
|
147
|
-
const rows = await this.#db.get_turn_context.all({
|
|
160
|
+
let rows = await this.#db.get_turn_context.all({
|
|
148
161
|
run_id: currentRunId,
|
|
149
162
|
turn,
|
|
150
163
|
});
|
|
151
|
-
const
|
|
164
|
+
const lastCtx = await this.#db.get_last_context_tokens.get({
|
|
165
|
+
run_id: currentRunId,
|
|
166
|
+
});
|
|
167
|
+
const lastContextTokens = lastCtx?.context_tokens ?? 0;
|
|
168
|
+
|
|
169
|
+
let messages = await ContextAssembler.assembleFromTurnContext(
|
|
152
170
|
rows,
|
|
153
171
|
{
|
|
154
172
|
type: mode,
|
|
155
173
|
systemPrompt,
|
|
156
174
|
contextSize,
|
|
175
|
+
demoted,
|
|
176
|
+
toolSet,
|
|
177
|
+
lastContextTokens,
|
|
157
178
|
},
|
|
158
179
|
this.#hooks,
|
|
159
180
|
);
|
|
160
181
|
|
|
182
|
+
const budgetResult = await this.#hooks.budget.enforce({
|
|
183
|
+
contextSize,
|
|
184
|
+
messages,
|
|
185
|
+
rows,
|
|
186
|
+
});
|
|
187
|
+
messages = budgetResult.messages;
|
|
188
|
+
rows = budgetResult.rows;
|
|
189
|
+
const assembledTokens =
|
|
190
|
+
budgetResult.assembledTokens ??
|
|
191
|
+
messages.reduce((sum, m) => sum + countTokens(m.content), 0);
|
|
192
|
+
|
|
193
|
+
// Budget overflow — return 413 to caller without calling LLM.
|
|
194
|
+
// Panic mode suppresses this — the model must run to free space.
|
|
195
|
+
if (budgetResult.status === 413 && mode !== "panic") {
|
|
196
|
+
return {
|
|
197
|
+
turn,
|
|
198
|
+
turnId: turnRow.id,
|
|
199
|
+
status: 413,
|
|
200
|
+
assembledTokens,
|
|
201
|
+
contextSize,
|
|
202
|
+
overflow: budgetResult.overflow,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
161
206
|
const filteredMessages = await this.#hooks.llm.messages.filter(messages, {
|
|
162
207
|
model: requestedModel,
|
|
163
208
|
projectId,
|
|
164
209
|
runId: currentRunId,
|
|
165
210
|
});
|
|
166
211
|
|
|
167
|
-
// Store assembled messages as audit
|
|
168
212
|
// Call LLM
|
|
169
213
|
await this.#hooks.llm.request.started.emit({ model: requestedModel, turn });
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
214
|
+
let rawResult;
|
|
215
|
+
const isTransient = (e) =>
|
|
216
|
+
/\b(503|429|timeout|ECONNREFUSED|ECONNRESET|unavailable)\b/i.test(
|
|
217
|
+
e.message,
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
for (let llmAttempt = 0; ; llmAttempt++) {
|
|
221
|
+
try {
|
|
222
|
+
rawResult = await this.#llmProvider.completion(
|
|
223
|
+
filteredMessages,
|
|
224
|
+
requestedModel,
|
|
225
|
+
{ temperature: options?.temperature, signal },
|
|
226
|
+
);
|
|
227
|
+
break;
|
|
228
|
+
} catch (err) {
|
|
229
|
+
if (isTransient(err) && llmAttempt < 3) {
|
|
230
|
+
const delay = 1000 * 2 ** llmAttempt;
|
|
231
|
+
console.warn(
|
|
232
|
+
`[RUMMY] Transient LLM error (attempt ${llmAttempt + 1}/3): ${err.message.slice(0, 120)}. Retrying in ${delay}ms.`,
|
|
233
|
+
);
|
|
234
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
throw err;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
175
240
|
const result = await this.#hooks.llm.response.filter(rawResult, {
|
|
176
241
|
model: requestedModel,
|
|
177
242
|
projectId,
|
|
@@ -195,6 +260,19 @@ export default class TurnExecutor {
|
|
|
195
260
|
// Parse and emit — plugins handle audit storage
|
|
196
261
|
const { commands, unparsed } = XmlParser.parse(content);
|
|
197
262
|
|
|
263
|
+
// Ensure reasoning_content captures both API field and <think> tag
|
|
264
|
+
if (responseMessage) {
|
|
265
|
+
const thinkCmds = commands.filter((c) => c.name === "think");
|
|
266
|
+
const thinkText = thinkCmds
|
|
267
|
+
.map((c) => c.body)
|
|
268
|
+
.filter(Boolean)
|
|
269
|
+
.join("\n");
|
|
270
|
+
const apiReasoning = responseMessage.reasoning_content || "";
|
|
271
|
+
const parts = [apiReasoning, thinkText].filter(Boolean);
|
|
272
|
+
responseMessage.reasoning_content =
|
|
273
|
+
parts.length > 0 ? parts.join("\n") : null;
|
|
274
|
+
}
|
|
275
|
+
|
|
198
276
|
const systemMsg = filteredMessages.find((m) => m.role === "system");
|
|
199
277
|
const userMsg = filteredMessages.find((m) => m.role === "user");
|
|
200
278
|
await this.#hooks.turn.response.emit({
|
|
@@ -205,98 +283,173 @@ export default class TurnExecutor {
|
|
|
205
283
|
content,
|
|
206
284
|
commands,
|
|
207
285
|
unparsed,
|
|
286
|
+
assembledTokens,
|
|
287
|
+
contextSize,
|
|
208
288
|
systemMsg: systemMsg?.content,
|
|
209
289
|
userMsg: userMsg?.content,
|
|
210
290
|
});
|
|
211
291
|
|
|
212
292
|
// --- PHASE 1: RECORD ---
|
|
213
|
-
//
|
|
293
|
+
// Split lifecycle signals from action commands.
|
|
294
|
+
// Lifecycle signals (summarize, update, unknown, known) are state
|
|
295
|
+
// declarations — always recorded, never 409'd by sequential dispatch.
|
|
296
|
+
const LIFECYCLE = new Set(["summarize", "update", "unknown", "known"]);
|
|
214
297
|
|
|
215
298
|
const recorded = [];
|
|
216
|
-
|
|
217
|
-
|
|
299
|
+
const lifecycle = [];
|
|
300
|
+
const actions = [];
|
|
218
301
|
|
|
219
302
|
for (const cmd of commands) {
|
|
220
|
-
const entry = await this.#record(
|
|
221
|
-
if (!entry) continue;
|
|
222
|
-
|
|
223
|
-
if (entry.scheme === "summarize") summaryText = entry.body;
|
|
224
|
-
else if (entry.scheme === "update") updateText = entry.body;
|
|
225
|
-
else recorded.push(entry);
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
// If model sent both, summary wins
|
|
229
|
-
if (summaryText && updateText) updateText = null;
|
|
230
|
-
|
|
231
|
-
// If model sent neither, heal from content
|
|
232
|
-
let statusHealed = false;
|
|
233
|
-
if (!summaryText && !updateText) {
|
|
234
|
-
const healed = ResponseHealer.healStatus(content, commands);
|
|
235
|
-
summaryText = healed.summaryText;
|
|
236
|
-
updateText = healed.updateText;
|
|
237
|
-
statusHealed = true;
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
// Record healed status
|
|
241
|
-
if (summaryText) {
|
|
242
|
-
const summaryPath = await this.#knownStore.slugPath(
|
|
243
|
-
currentRunId,
|
|
244
|
-
"summarize",
|
|
245
|
-
summaryText,
|
|
246
|
-
);
|
|
247
|
-
await this.#knownStore.upsert(
|
|
248
|
-
currentRunId,
|
|
249
|
-
turn,
|
|
250
|
-
summaryPath,
|
|
251
|
-
summaryText,
|
|
252
|
-
"summary",
|
|
253
|
-
);
|
|
254
|
-
} else if (updateText) {
|
|
255
|
-
const updatePath = await this.#knownStore.slugPath(
|
|
256
|
-
currentRunId,
|
|
257
|
-
"update",
|
|
258
|
-
updateText,
|
|
259
|
-
);
|
|
260
|
-
await this.#knownStore.upsert(
|
|
303
|
+
const entry = await this.#record(
|
|
261
304
|
currentRunId,
|
|
305
|
+
currentLoopId,
|
|
262
306
|
turn,
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
"info",
|
|
307
|
+
mode,
|
|
308
|
+
cmd,
|
|
266
309
|
);
|
|
310
|
+
if (!entry) continue;
|
|
311
|
+
recorded.push(entry);
|
|
312
|
+
|
|
313
|
+
if (LIFECYCLE.has(entry.scheme)) {
|
|
314
|
+
lifecycle.push(entry);
|
|
315
|
+
} else {
|
|
316
|
+
actions.push(entry);
|
|
317
|
+
}
|
|
267
318
|
}
|
|
268
319
|
|
|
269
320
|
// --- PHASE 2: DISPATCH ---
|
|
270
|
-
//
|
|
321
|
+
// Budget plugin activates the guard on the store for dispatch.
|
|
322
|
+
const guard = this.#hooks.budget.activate(
|
|
323
|
+
this.#knownStore,
|
|
324
|
+
contextSize,
|
|
325
|
+
assembledTokens,
|
|
326
|
+
);
|
|
327
|
+
const { BudgetExceeded } = this.#hooks.budget;
|
|
271
328
|
|
|
272
329
|
let hasErrors = false;
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
330
|
+
let hasProposed = false;
|
|
331
|
+
let abortAfter = null;
|
|
332
|
+
const dispatched = [...lifecycle];
|
|
333
|
+
|
|
334
|
+
try {
|
|
335
|
+
// Lifecycle signals first — always dispatched, never aborted.
|
|
336
|
+
for (const entry of lifecycle) {
|
|
337
|
+
await this.#hooks.tool.before.emit({ entry, rummy });
|
|
338
|
+
await this.#hooks.tools.dispatch(entry.scheme, entry, rummy);
|
|
339
|
+
await this.#hooks.tool.after.emit({ entry, rummy });
|
|
340
|
+
await this.#hooks.entry.created.emit(entry);
|
|
341
|
+
}
|
|
277
342
|
|
|
278
|
-
|
|
279
|
-
|
|
343
|
+
for (const entry of actions) {
|
|
344
|
+
if (abortAfter || guard.isTripped) {
|
|
345
|
+
await this.#knownStore.upsert(
|
|
346
|
+
currentRunId,
|
|
347
|
+
turn,
|
|
348
|
+
entry.resultPath || entry.path,
|
|
349
|
+
"",
|
|
350
|
+
guard.isTripped ? 413 : 409,
|
|
351
|
+
{
|
|
352
|
+
attributes: {
|
|
353
|
+
error: guard.isTripped
|
|
354
|
+
? `Budget exceeded by <${guard.tripSource}>.`
|
|
355
|
+
: `Aborted — preceding <${abortAfter}> requires resolution.`,
|
|
356
|
+
},
|
|
357
|
+
loopId: currentLoopId,
|
|
358
|
+
},
|
|
359
|
+
);
|
|
360
|
+
hasErrors = true;
|
|
361
|
+
continue;
|
|
362
|
+
}
|
|
280
363
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
364
|
+
try {
|
|
365
|
+
await this.#hooks.tool.before.emit({ entry, rummy });
|
|
366
|
+
await this.#hooks.tools.dispatch(entry.scheme, entry, rummy);
|
|
367
|
+
await this.#hooks.tool.after.emit({ entry, rummy });
|
|
368
|
+
await this.#hooks.entry.created.emit(entry);
|
|
369
|
+
dispatched.push(entry);
|
|
370
|
+
} catch (err) {
|
|
371
|
+
if (err instanceof BudgetExceeded) {
|
|
372
|
+
guard.trip(entry.scheme);
|
|
373
|
+
await this.#knownStore.upsert(
|
|
374
|
+
currentRunId,
|
|
375
|
+
turn,
|
|
376
|
+
entry.resultPath || entry.path,
|
|
377
|
+
`Budget exceeded: ${err.requested} tokens requested, ${err.remaining} remaining.`,
|
|
378
|
+
413,
|
|
379
|
+
{ attributes: { error: err.message }, loopId: currentLoopId },
|
|
380
|
+
);
|
|
381
|
+
hasErrors = true;
|
|
382
|
+
abortAfter = entry.scheme;
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
385
|
+
throw err;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const row = await this.#db.get_entry_state.get({
|
|
389
|
+
run_id: currentRunId,
|
|
390
|
+
path: entry.resultPath || entry.path,
|
|
391
|
+
});
|
|
392
|
+
if (row?.status === 202) {
|
|
393
|
+
hasProposed = true;
|
|
394
|
+
abortAfter = entry.scheme;
|
|
395
|
+
} else if (row?.status >= 400) {
|
|
396
|
+
hasErrors = true;
|
|
397
|
+
abortAfter = entry.scheme;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Materialize proposals only if we dispatched actions
|
|
402
|
+
if (!abortAfter || hasProposed) {
|
|
403
|
+
await this.#hooks.turn.proposing.emit({ rummy, recorded: dispatched });
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// Recheck after materialization (set handler may create proposals)
|
|
407
|
+
if (!hasProposed && !hasErrors) {
|
|
408
|
+
for (const entry of actions) {
|
|
409
|
+
const row = await this.#db.get_entry_state.get({
|
|
410
|
+
run_id: currentRunId,
|
|
411
|
+
path: entry.resultPath || entry.path,
|
|
412
|
+
});
|
|
413
|
+
if (row?.status === 202) hasProposed = true;
|
|
414
|
+
if (row?.status >= 400) hasErrors = true;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
} finally {
|
|
418
|
+
this.#hooks.budget.deactivate(this.#knownStore);
|
|
288
419
|
}
|
|
289
420
|
|
|
290
|
-
//
|
|
291
|
-
|
|
421
|
+
// Lifecycle signals are always available — never 409'd.
|
|
422
|
+
const summaryEntry = lifecycle.find((e) => e.scheme === "summarize");
|
|
423
|
+
const updateEntry = lifecycle.find((e) => e.scheme === "update");
|
|
424
|
+
let summaryText = summaryEntry?.body || null;
|
|
425
|
+
let updateText = updateEntry?.body || null;
|
|
426
|
+
|
|
427
|
+
// If model sent both, update wins — if it can't decide, it's not done
|
|
428
|
+
if (summaryText && updateText) summaryText = null;
|
|
429
|
+
|
|
430
|
+
// If model says "done" but actions failed, override — the model's
|
|
431
|
+
// assertion that it's done is false if it failed to do what it tried.
|
|
432
|
+
if (summaryText && hasErrors) {
|
|
433
|
+
console.warn(
|
|
434
|
+
"[RUMMY] Overriding <summarize> — actions in this turn failed. Continuing.",
|
|
435
|
+
);
|
|
436
|
+
updateText = summaryText;
|
|
292
437
|
summaryText = null;
|
|
293
|
-
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// If model sent neither, heal from content
|
|
441
|
+
let statusHealed = false;
|
|
442
|
+
if (!summaryText && !updateText) {
|
|
443
|
+
const healed = ResponseHealer.healStatus(content, commands);
|
|
444
|
+
summaryText = healed.summaryText;
|
|
445
|
+
updateText = healed.updateText;
|
|
446
|
+
statusHealed = true;
|
|
294
447
|
}
|
|
295
448
|
|
|
296
449
|
// --- Classify for return value ---
|
|
297
450
|
|
|
298
451
|
const actionCalls = recorded.filter((e) =>
|
|
299
|
-
["get", "
|
|
452
|
+
["get", "set", "rm", "mv", "cp", "sh", "env", "search"].includes(
|
|
300
453
|
e.scheme,
|
|
301
454
|
),
|
|
302
455
|
);
|
|
@@ -318,7 +471,7 @@ export default class TurnExecutor {
|
|
|
318
471
|
|
|
319
472
|
const askUserEntry = recorded.find((e) => e.scheme === "ask_user");
|
|
320
473
|
|
|
321
|
-
|
|
474
|
+
const turnResult = {
|
|
322
475
|
turn,
|
|
323
476
|
turnId: turnRow.id,
|
|
324
477
|
actionCalls,
|
|
@@ -335,25 +488,32 @@ export default class TurnExecutor {
|
|
|
335
488
|
options?.temperature ??
|
|
336
489
|
Number.parseFloat(process.env.RUMMY_TEMPERATURE || "0.7"),
|
|
337
490
|
contextSize,
|
|
491
|
+
assembledTokens,
|
|
338
492
|
usage: result.usage,
|
|
339
493
|
};
|
|
494
|
+
|
|
495
|
+
await this.#hooks.turn.completed.emit(turnResult);
|
|
496
|
+
|
|
497
|
+
return turnResult;
|
|
340
498
|
}
|
|
341
499
|
|
|
342
500
|
/**
|
|
343
501
|
* Record a parsed command as a known_entries row.
|
|
344
502
|
* Returns the recorded entry descriptor, or null if rejected/skipped.
|
|
345
503
|
*/
|
|
346
|
-
async #record(runId, turn, mode, cmd) {
|
|
347
|
-
// Mode enforcement — reject prohibited commands in ask mode
|
|
348
|
-
if (mode === "ask") {
|
|
504
|
+
async #record(runId, loopId, turn, mode, cmd) {
|
|
505
|
+
// Mode enforcement — reject prohibited commands in ask/panic mode
|
|
506
|
+
if (mode === "ask" || mode === "panic") {
|
|
349
507
|
if (cmd.name === "sh") {
|
|
350
508
|
console.warn("[RUMMY] Rejected <sh> in ask mode");
|
|
351
509
|
return null;
|
|
352
510
|
}
|
|
353
|
-
if (cmd.name === "set" && cmd.path) {
|
|
511
|
+
if (cmd.name === "set" && cmd.path && cmd.body) {
|
|
354
512
|
const scheme = KnownStore.scheme(cmd.path);
|
|
355
513
|
if (scheme === null) {
|
|
356
|
-
console.warn(
|
|
514
|
+
console.warn(
|
|
515
|
+
`[RUMMY] Rejected file edit to ${cmd.path} in ${mode} mode`,
|
|
516
|
+
);
|
|
357
517
|
return null;
|
|
358
518
|
}
|
|
359
519
|
}
|
|
@@ -377,21 +537,40 @@ export default class TurnExecutor {
|
|
|
377
537
|
|
|
378
538
|
const scheme = cmd.name;
|
|
379
539
|
|
|
380
|
-
// Structural tags —
|
|
540
|
+
// Structural tags — recorded like any other entry
|
|
381
541
|
if (scheme === "summarize" || scheme === "update") {
|
|
382
|
-
|
|
542
|
+
const statusPath = await this.#knownStore.slugPath(
|
|
543
|
+
runId,
|
|
544
|
+
scheme,
|
|
545
|
+
cmd.body,
|
|
546
|
+
);
|
|
547
|
+
await this.#knownStore.upsert(runId, turn, statusPath, cmd.body, 200, {
|
|
548
|
+
loopId,
|
|
549
|
+
});
|
|
550
|
+
return {
|
|
551
|
+
scheme,
|
|
552
|
+
body: cmd.body,
|
|
553
|
+
path: statusPath,
|
|
554
|
+
resultPath: statusPath,
|
|
555
|
+
attributes: null,
|
|
556
|
+
};
|
|
383
557
|
}
|
|
384
558
|
|
|
385
559
|
// Unknown — deduplicated, sticky
|
|
386
560
|
if (scheme === "unknown") {
|
|
387
561
|
const existingValues = await this.#knownStore.getUnknownValues(runId);
|
|
388
|
-
if (existingValues.has(cmd.body))
|
|
562
|
+
if (existingValues.has(cmd.body)) {
|
|
563
|
+
console.warn(`[RUMMY] Unknown deduped: "${cmd.body.slice(0, 60)}"`);
|
|
564
|
+
return null;
|
|
565
|
+
}
|
|
389
566
|
const unknownPath = await this.#knownStore.slugPath(
|
|
390
567
|
runId,
|
|
391
568
|
"unknown",
|
|
392
569
|
cmd.body,
|
|
393
570
|
);
|
|
394
|
-
await this.#knownStore.upsert(runId, turn, unknownPath, cmd.body,
|
|
571
|
+
await this.#knownStore.upsert(runId, turn, unknownPath, cmd.body, 200, {
|
|
572
|
+
loopId,
|
|
573
|
+
});
|
|
395
574
|
return {
|
|
396
575
|
scheme,
|
|
397
576
|
path: unknownPath,
|
|
@@ -401,15 +580,14 @@ export default class TurnExecutor {
|
|
|
401
580
|
};
|
|
402
581
|
}
|
|
403
582
|
|
|
404
|
-
// Normalize path — encode spaces in scheme:// paths
|
|
405
583
|
const rawTarget = cmd.path || cmd.command || cmd.question || "";
|
|
406
|
-
const target = rawTarget
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
584
|
+
const target = rawTarget;
|
|
585
|
+
const resultPath = await this.#knownStore.dedup(
|
|
586
|
+
runId,
|
|
587
|
+
scheme,
|
|
588
|
+
target,
|
|
589
|
+
turn,
|
|
590
|
+
);
|
|
413
591
|
|
|
414
592
|
// Pass parsed command fields through as attributes
|
|
415
593
|
const { name: _, ...attributes } = cmd;
|
|
@@ -418,9 +596,72 @@ export default class TurnExecutor {
|
|
|
418
596
|
// known tool or naked write → known:// slug from body
|
|
419
597
|
if (scheme === "known" || (scheme === "set" && !cmd.path)) {
|
|
420
598
|
if (!cmd.body) return null;
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
599
|
+
|
|
600
|
+
// Size gate: reject entries > 512 tokens — force atomic entries
|
|
601
|
+
const entryTokens = countTokens(cmd.body);
|
|
602
|
+
const MAX_ENTRY_TOKENS = 512;
|
|
603
|
+
if (scheme === "known" && entryTokens > MAX_ENTRY_TOKENS) {
|
|
604
|
+
const rejectPath = await this.#knownStore.slugPath(
|
|
605
|
+
runId,
|
|
606
|
+
scheme,
|
|
607
|
+
cmd.body,
|
|
608
|
+
);
|
|
609
|
+
await this.#knownStore.upsert(
|
|
610
|
+
runId,
|
|
611
|
+
turn,
|
|
612
|
+
rejectPath,
|
|
613
|
+
`Entry too large (${entryTokens} tokens, max ${MAX_ENTRY_TOKENS}). Sort the information, ideas, or plans carefully into multiple entries.`,
|
|
614
|
+
413,
|
|
615
|
+
{ loopId },
|
|
616
|
+
);
|
|
617
|
+
return {
|
|
618
|
+
scheme,
|
|
619
|
+
path: rejectPath,
|
|
620
|
+
body: "",
|
|
621
|
+
resultPath: rejectPath,
|
|
622
|
+
attributes,
|
|
623
|
+
status: 413,
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
let knownPath = cmd.path;
|
|
628
|
+
if (!knownPath) {
|
|
629
|
+
knownPath = await this.#knownStore.slugPath(
|
|
630
|
+
runId,
|
|
631
|
+
"known",
|
|
632
|
+
cmd.body,
|
|
633
|
+
cmd.summary,
|
|
634
|
+
);
|
|
635
|
+
}
|
|
636
|
+
// Dedup: if this exact path already exists, update rather than duplicate
|
|
637
|
+
const existing = await this.#knownStore.getEntriesByPattern(
|
|
638
|
+
runId,
|
|
639
|
+
knownPath,
|
|
640
|
+
null,
|
|
641
|
+
);
|
|
642
|
+
if (existing.length > 0) {
|
|
643
|
+
// Path exists — update body and turn, skip creating a new entry
|
|
644
|
+
await this.#knownStore.upsert(
|
|
645
|
+
runId,
|
|
646
|
+
turn,
|
|
647
|
+
existing[0].path,
|
|
648
|
+
cmd.body || existing[0].body,
|
|
649
|
+
200,
|
|
650
|
+
{
|
|
651
|
+
loopId,
|
|
652
|
+
},
|
|
653
|
+
);
|
|
654
|
+
return {
|
|
655
|
+
scheme: "known",
|
|
656
|
+
path: existing[0].path,
|
|
657
|
+
body: cmd.body || existing[0].body,
|
|
658
|
+
resultPath: existing[0].path,
|
|
659
|
+
attributes,
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
await this.#knownStore.upsert(runId, turn, knownPath, cmd.body, 200, {
|
|
663
|
+
loopId,
|
|
664
|
+
});
|
|
424
665
|
return {
|
|
425
666
|
scheme: "known",
|
|
426
667
|
path: knownPath,
|
|
@@ -430,28 +671,65 @@ export default class TurnExecutor {
|
|
|
430
671
|
};
|
|
431
672
|
}
|
|
432
673
|
|
|
433
|
-
// Record the entry
|
|
434
674
|
const body = cmd.body || cmd.command || cmd.question || "";
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
675
|
+
|
|
676
|
+
// Filter: plugins can validate/transform before recording
|
|
677
|
+
const filtered = await this.#hooks.entry.recording.filter(
|
|
678
|
+
{ scheme, path: resultPath, body, attributes, status: 200 },
|
|
679
|
+
{ runId, turn, loopId },
|
|
680
|
+
);
|
|
681
|
+
if (filtered.status >= 400) return filtered;
|
|
682
|
+
|
|
683
|
+
// Record the entry — 200 OK, handlers change status during dispatch
|
|
684
|
+
await this.#knownStore.upsert(
|
|
685
|
+
runId,
|
|
686
|
+
turn,
|
|
687
|
+
filtered.path,
|
|
688
|
+
filtered.body,
|
|
689
|
+
200,
|
|
690
|
+
{
|
|
691
|
+
attributes: filtered.attributes,
|
|
692
|
+
loopId,
|
|
693
|
+
},
|
|
694
|
+
);
|
|
439
695
|
|
|
440
696
|
return {
|
|
441
|
-
scheme,
|
|
442
|
-
path:
|
|
443
|
-
body,
|
|
444
|
-
attributes,
|
|
445
|
-
|
|
446
|
-
resultPath,
|
|
697
|
+
scheme: filtered.scheme,
|
|
698
|
+
path: filtered.path,
|
|
699
|
+
body: filtered.body,
|
|
700
|
+
attributes: filtered.attributes,
|
|
701
|
+
status: 200,
|
|
702
|
+
resultPath: filtered.path,
|
|
447
703
|
};
|
|
448
704
|
}
|
|
449
705
|
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
706
|
+
async #rematerialize(runId, loopId, turn) {
|
|
707
|
+
await this.#db.clear_turn_context.run({ run_id: runId, turn });
|
|
708
|
+
const viewRows = await this.#db.get_model_context.all({ run_id: runId });
|
|
709
|
+
for (const row of viewRows) {
|
|
710
|
+
const scheme = row.scheme || "file";
|
|
711
|
+
const projectedBody = await this.#hooks.tools.view(scheme, {
|
|
712
|
+
path: row.path,
|
|
713
|
+
scheme,
|
|
714
|
+
body: row.body,
|
|
715
|
+
attributes: row.attributes ? JSON.parse(row.attributes) : null,
|
|
716
|
+
fidelity: row.fidelity,
|
|
717
|
+
category: row.category,
|
|
718
|
+
});
|
|
719
|
+
await this.#db.insert_turn_context.run({
|
|
720
|
+
run_id: runId,
|
|
721
|
+
loop_id: loopId,
|
|
722
|
+
turn,
|
|
723
|
+
ordinal: row.ordinal,
|
|
724
|
+
path: row.path,
|
|
725
|
+
fidelity: row.fidelity,
|
|
726
|
+
status: row.status,
|
|
727
|
+
body: projectedBody ?? "",
|
|
728
|
+
tokens: countTokens(projectedBody ?? ""),
|
|
729
|
+
attributes: row.attributes,
|
|
730
|
+
category: row.category,
|
|
731
|
+
source_turn: row.turn,
|
|
732
|
+
});
|
|
733
|
+
}
|
|
456
734
|
}
|
|
457
735
|
}
|