@possumtech/rummy 0.2.8 → 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 +11 -1
- package/EXCEPTIONS.md +46 -0
- package/PLUGINS.md +422 -188
- package/SPEC.md +284 -93
- package/migrations/001_initial_schema.sql +6 -4
- package/package.json +13 -5
- package/src/agent/AgentLoop.js +166 -15
- package/src/agent/ContextAssembler.js +18 -4
- package/src/agent/KnownStore.js +127 -13
- package/src/agent/ProjectAgent.js +4 -1
- package/src/agent/ResponseHealer.js +21 -1
- package/src/agent/TurnExecutor.js +365 -175
- package/src/agent/XmlParser.js +72 -39
- package/src/agent/known_store.sql +20 -4
- package/src/agent/schemes.sql +3 -0
- package/src/agent/tokens.js +6 -21
- package/src/agent/turns.sql +10 -1
- package/src/hooks/Hooks.js +18 -0
- package/src/hooks/PluginContext.js +14 -1
- package/src/hooks/RummyContext.js +16 -4
- 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 +5 -5
- 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 +10 -6
- package/src/plugins/cp/cpDoc.js +29 -0
- package/src/plugins/current/README.md +4 -4
- package/src/plugins/current/current.js +9 -6
- package/src/plugins/engine/engine.sql +1 -8
- package/src/plugins/engine/turn_context.sql +4 -9
- package/src/plugins/env/README.md +3 -4
- package/src/plugins/env/env.js +5 -5
- package/src/plugins/env/envDoc.js +29 -0
- package/src/plugins/file/README.md +9 -12
- package/src/plugins/file/file.js +34 -35
- package/src/plugins/get/README.md +2 -2
- package/src/plugins/get/get.js +6 -5
- package/src/plugins/get/getDoc.js +41 -0
- package/src/plugins/hedberg/hedberg.js +2 -1
- package/src/plugins/hedberg/normalize.js +28 -0
- package/src/plugins/hedberg/patterns.js +25 -27
- package/src/plugins/hedberg/sed.js +17 -10
- package/src/plugins/index.js +66 -14
- package/src/plugins/instructions/README.md +6 -2
- package/src/plugins/instructions/instructions.js +20 -4
- package/src/plugins/instructions/preamble.md +9 -5
- package/src/plugins/known/README.md +10 -7
- package/src/plugins/known/known.js +29 -17
- package/src/plugins/known/knownDoc.js +33 -0
- package/src/plugins/mv/README.md +5 -4
- package/src/plugins/mv/mv.js +10 -6
- 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 +9 -6
- package/src/plugins/progress/progress.js +41 -15
- package/src/plugins/prompt/README.md +5 -5
- package/src/plugins/prompt/prompt.js +18 -13
- package/src/plugins/rm/README.md +4 -4
- package/src/plugins/rm/rm.js +5 -5
- package/src/plugins/rm/rmDoc.js +30 -0
- package/src/plugins/rpc/README.md +15 -28
- package/src/plugins/rpc/rpc.js +42 -77
- package/src/plugins/set/README.md +13 -12
- package/src/plugins/set/set.js +60 -5
- package/src/plugins/set/setDoc.js +45 -0
- package/src/plugins/sh/README.md +4 -4
- package/src/plugins/sh/sh.js +5 -5
- package/src/plugins/sh/shDoc.js +29 -0
- package/src/plugins/{skills/skills.js → skill/skill.js} +10 -51
- 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 +3 -1
- 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 +9 -7
- 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/RpcRegistry.js +52 -4
- package/src/sql/v_model_context.sql +16 -21
- package/src/plugins/ask_user/docs.md +0 -2
- package/src/plugins/cp/docs.md +0 -2
- package/src/plugins/env/docs.md +0 -4
- package/src/plugins/get/docs.md +0 -10
- package/src/plugins/known/docs.md +0 -3
- package/src/plugins/mv/docs.md +0 -2
- package/src/plugins/rm/docs.md +0 -6
- package/src/plugins/set/docs.md +0 -6
- 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 -6
- package/src/plugins/store/store.js +0 -63
- package/src/plugins/summarize/docs.md +0 -4
- package/src/plugins/unknown/docs.md +0 -5
- package/src/plugins/update/docs.md +0 -4
|
@@ -28,7 +28,8 @@ export default class TurnExecutor {
|
|
|
28
28
|
currentLoopId,
|
|
29
29
|
requestedModel,
|
|
30
30
|
loopPrompt,
|
|
31
|
-
|
|
31
|
+
noRepo,
|
|
32
|
+
toolSet,
|
|
32
33
|
contextSize,
|
|
33
34
|
options,
|
|
34
35
|
signal,
|
|
@@ -71,7 +72,8 @@ export default class TurnExecutor {
|
|
|
71
72
|
runId: currentRunId,
|
|
72
73
|
loopId: currentLoopId,
|
|
73
74
|
turnId: turnRow.id,
|
|
74
|
-
|
|
75
|
+
noRepo,
|
|
76
|
+
toolSet,
|
|
75
77
|
contextSize,
|
|
76
78
|
systemPrompt: null,
|
|
77
79
|
loopPrompt,
|
|
@@ -142,6 +144,12 @@ export default class TurnExecutor {
|
|
|
142
144
|
|
|
143
145
|
const demoted = [];
|
|
144
146
|
|
|
147
|
+
await this.#hooks.context.materialized.emit({
|
|
148
|
+
runId: currentRunId,
|
|
149
|
+
turn,
|
|
150
|
+
rowCount: viewRows.length,
|
|
151
|
+
});
|
|
152
|
+
|
|
145
153
|
await this.#hooks.run.progress.emit({
|
|
146
154
|
projectId,
|
|
147
155
|
run: currentAlias,
|
|
@@ -149,11 +157,15 @@ export default class TurnExecutor {
|
|
|
149
157
|
status: "thinking",
|
|
150
158
|
});
|
|
151
159
|
|
|
152
|
-
// Assemble messages from projected system prompt + materialized turn_context
|
|
153
160
|
let rows = await this.#db.get_turn_context.all({
|
|
154
161
|
run_id: currentRunId,
|
|
155
162
|
turn,
|
|
156
163
|
});
|
|
164
|
+
const lastCtx = await this.#db.get_last_context_tokens.get({
|
|
165
|
+
run_id: currentRunId,
|
|
166
|
+
});
|
|
167
|
+
const lastContextTokens = lastCtx?.context_tokens ?? 0;
|
|
168
|
+
|
|
157
169
|
let messages = await ContextAssembler.assembleFromTurnContext(
|
|
158
170
|
rows,
|
|
159
171
|
{
|
|
@@ -161,83 +173,34 @@ export default class TurnExecutor {
|
|
|
161
173
|
systemPrompt,
|
|
162
174
|
contextSize,
|
|
163
175
|
demoted,
|
|
176
|
+
toolSet,
|
|
177
|
+
lastContextTokens,
|
|
164
178
|
},
|
|
165
179
|
this.#hooks,
|
|
166
180
|
);
|
|
167
181
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
entry.path,
|
|
191
|
-
"summary",
|
|
192
|
-
);
|
|
193
|
-
excess -= entry.tokens;
|
|
194
|
-
demoted.push(entry.path);
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
if (demoted.length > 0) {
|
|
198
|
-
await this.#db.clear_turn_context.run({ run_id: currentRunId, turn });
|
|
199
|
-
const freshViewRows = await this.#db.get_model_context.all({
|
|
200
|
-
run_id: currentRunId,
|
|
201
|
-
});
|
|
202
|
-
for (const row of freshViewRows) {
|
|
203
|
-
const scheme = row.scheme || "file";
|
|
204
|
-
const projectedBody = await this.#hooks.tools.view(scheme, {
|
|
205
|
-
path: row.path,
|
|
206
|
-
scheme,
|
|
207
|
-
body: row.body,
|
|
208
|
-
attributes: row.attributes ? JSON.parse(row.attributes) : null,
|
|
209
|
-
fidelity: row.fidelity,
|
|
210
|
-
category: row.category,
|
|
211
|
-
});
|
|
212
|
-
await this.#db.insert_turn_context.run({
|
|
213
|
-
run_id: currentRunId,
|
|
214
|
-
loop_id: currentLoopId,
|
|
215
|
-
turn,
|
|
216
|
-
ordinal: row.ordinal,
|
|
217
|
-
path: row.path,
|
|
218
|
-
fidelity: row.fidelity,
|
|
219
|
-
status: row.status,
|
|
220
|
-
body: projectedBody ?? "",
|
|
221
|
-
tokens: countTokens(projectedBody ?? ""),
|
|
222
|
-
attributes: row.attributes,
|
|
223
|
-
category: row.category,
|
|
224
|
-
source_turn: row.turn,
|
|
225
|
-
});
|
|
226
|
-
}
|
|
227
|
-
rows = await this.#db.get_turn_context.all({
|
|
228
|
-
run_id: currentRunId,
|
|
229
|
-
turn,
|
|
230
|
-
});
|
|
231
|
-
messages = await ContextAssembler.assembleFromTurnContext(
|
|
232
|
-
rows,
|
|
233
|
-
{ type: mode, systemPrompt, contextSize, demoted },
|
|
234
|
-
this.#hooks,
|
|
235
|
-
);
|
|
236
|
-
console.warn(
|
|
237
|
-
`[RUMMY] Budget exceeded: demoted ${demoted.length} entries to fit ${contextSize} token limit`,
|
|
238
|
-
);
|
|
239
|
-
}
|
|
240
|
-
}
|
|
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
|
+
};
|
|
241
204
|
}
|
|
242
205
|
|
|
243
206
|
const filteredMessages = await this.#hooks.llm.messages.filter(messages, {
|
|
@@ -246,14 +209,34 @@ export default class TurnExecutor {
|
|
|
246
209
|
runId: currentRunId,
|
|
247
210
|
});
|
|
248
211
|
|
|
249
|
-
// Store assembled messages as audit
|
|
250
212
|
// Call LLM
|
|
251
213
|
await this.#hooks.llm.request.started.emit({ model: requestedModel, turn });
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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
|
+
}
|
|
257
240
|
const result = await this.#hooks.llm.response.filter(rawResult, {
|
|
258
241
|
model: requestedModel,
|
|
259
242
|
projectId,
|
|
@@ -277,6 +260,19 @@ export default class TurnExecutor {
|
|
|
277
260
|
// Parse and emit — plugins handle audit storage
|
|
278
261
|
const { commands, unparsed } = XmlParser.parse(content);
|
|
279
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
|
+
|
|
280
276
|
const systemMsg = filteredMessages.find((m) => m.role === "system");
|
|
281
277
|
const userMsg = filteredMessages.find((m) => m.role === "user");
|
|
282
278
|
await this.#hooks.turn.response.emit({
|
|
@@ -287,16 +283,21 @@ export default class TurnExecutor {
|
|
|
287
283
|
content,
|
|
288
284
|
commands,
|
|
289
285
|
unparsed,
|
|
286
|
+
assembledTokens,
|
|
287
|
+
contextSize,
|
|
290
288
|
systemMsg: systemMsg?.content,
|
|
291
289
|
userMsg: userMsg?.content,
|
|
292
290
|
});
|
|
293
291
|
|
|
294
292
|
// --- PHASE 1: RECORD ---
|
|
295
|
-
//
|
|
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"]);
|
|
296
297
|
|
|
297
298
|
const recorded = [];
|
|
298
|
-
|
|
299
|
-
|
|
299
|
+
const lifecycle = [];
|
|
300
|
+
const actions = [];
|
|
300
301
|
|
|
301
302
|
for (const cmd of commands) {
|
|
302
303
|
const entry = await this.#record(
|
|
@@ -307,94 +308,148 @@ export default class TurnExecutor {
|
|
|
307
308
|
cmd,
|
|
308
309
|
);
|
|
309
310
|
if (!entry) continue;
|
|
311
|
+
recorded.push(entry);
|
|
310
312
|
|
|
311
|
-
if (entry.scheme
|
|
312
|
-
|
|
313
|
-
else
|
|
313
|
+
if (LIFECYCLE.has(entry.scheme)) {
|
|
314
|
+
lifecycle.push(entry);
|
|
315
|
+
} else {
|
|
316
|
+
actions.push(entry);
|
|
317
|
+
}
|
|
314
318
|
}
|
|
315
319
|
|
|
316
|
-
//
|
|
317
|
-
|
|
320
|
+
// --- PHASE 2: DISPATCH ---
|
|
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;
|
|
318
328
|
|
|
319
|
-
|
|
320
|
-
let
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
329
|
+
let hasErrors = false;
|
|
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
|
+
}
|
|
327
342
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
);
|
|
349
|
-
await this.#knownStore.upsert(
|
|
350
|
-
currentRunId,
|
|
351
|
-
turn,
|
|
352
|
-
updatePath,
|
|
353
|
-
updateText,
|
|
354
|
-
200,
|
|
355
|
-
{ loopId: currentLoopId },
|
|
356
|
-
);
|
|
357
|
-
}
|
|
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
|
+
}
|
|
358
363
|
|
|
359
|
-
|
|
360
|
-
|
|
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
|
+
}
|
|
361
387
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
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
|
+
}
|
|
366
400
|
|
|
367
|
-
|
|
368
|
-
|
|
401
|
+
// Materialize proposals only if we dispatched actions
|
|
402
|
+
if (!abortAfter || hasProposed) {
|
|
403
|
+
await this.#hooks.turn.proposing.emit({ rummy, recorded: dispatched });
|
|
404
|
+
}
|
|
369
405
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
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);
|
|
380
419
|
}
|
|
381
420
|
|
|
382
|
-
//
|
|
383
|
-
|
|
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;
|
|
384
437
|
summaryText = null;
|
|
385
|
-
updateText = "Tool errors detected — retry or investigate.";
|
|
386
438
|
}
|
|
387
439
|
|
|
388
|
-
//
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
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;
|
|
392
447
|
}
|
|
393
448
|
|
|
394
449
|
// --- Classify for return value ---
|
|
395
450
|
|
|
396
451
|
const actionCalls = recorded.filter((e) =>
|
|
397
|
-
["get", "
|
|
452
|
+
["get", "set", "rm", "mv", "cp", "sh", "env", "search"].includes(
|
|
398
453
|
e.scheme,
|
|
399
454
|
),
|
|
400
455
|
);
|
|
@@ -416,7 +471,7 @@ export default class TurnExecutor {
|
|
|
416
471
|
|
|
417
472
|
const askUserEntry = recorded.find((e) => e.scheme === "ask_user");
|
|
418
473
|
|
|
419
|
-
|
|
474
|
+
const turnResult = {
|
|
420
475
|
turn,
|
|
421
476
|
turnId: turnRow.id,
|
|
422
477
|
actionCalls,
|
|
@@ -433,8 +488,13 @@ export default class TurnExecutor {
|
|
|
433
488
|
options?.temperature ??
|
|
434
489
|
Number.parseFloat(process.env.RUMMY_TEMPERATURE || "0.7"),
|
|
435
490
|
contextSize,
|
|
491
|
+
assembledTokens,
|
|
436
492
|
usage: result.usage,
|
|
437
493
|
};
|
|
494
|
+
|
|
495
|
+
await this.#hooks.turn.completed.emit(turnResult);
|
|
496
|
+
|
|
497
|
+
return turnResult;
|
|
438
498
|
}
|
|
439
499
|
|
|
440
500
|
/**
|
|
@@ -442,16 +502,18 @@ export default class TurnExecutor {
|
|
|
442
502
|
* Returns the recorded entry descriptor, or null if rejected/skipped.
|
|
443
503
|
*/
|
|
444
504
|
async #record(runId, loopId, turn, mode, cmd) {
|
|
445
|
-
// Mode enforcement — reject prohibited commands in ask mode
|
|
446
|
-
if (mode === "ask") {
|
|
505
|
+
// Mode enforcement — reject prohibited commands in ask/panic mode
|
|
506
|
+
if (mode === "ask" || mode === "panic") {
|
|
447
507
|
if (cmd.name === "sh") {
|
|
448
508
|
console.warn("[RUMMY] Rejected <sh> in ask mode");
|
|
449
509
|
return null;
|
|
450
510
|
}
|
|
451
|
-
if (cmd.name === "set" && cmd.path) {
|
|
511
|
+
if (cmd.name === "set" && cmd.path && cmd.body) {
|
|
452
512
|
const scheme = KnownStore.scheme(cmd.path);
|
|
453
513
|
if (scheme === null) {
|
|
454
|
-
console.warn(
|
|
514
|
+
console.warn(
|
|
515
|
+
`[RUMMY] Rejected file edit to ${cmd.path} in ${mode} mode`,
|
|
516
|
+
);
|
|
455
517
|
return null;
|
|
456
518
|
}
|
|
457
519
|
}
|
|
@@ -475,15 +537,32 @@ export default class TurnExecutor {
|
|
|
475
537
|
|
|
476
538
|
const scheme = cmd.name;
|
|
477
539
|
|
|
478
|
-
// Structural tags —
|
|
540
|
+
// Structural tags — recorded like any other entry
|
|
479
541
|
if (scheme === "summarize" || scheme === "update") {
|
|
480
|
-
|
|
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
|
+
};
|
|
481
557
|
}
|
|
482
558
|
|
|
483
559
|
// Unknown — deduplicated, sticky
|
|
484
560
|
if (scheme === "unknown") {
|
|
485
561
|
const existingValues = await this.#knownStore.getUnknownValues(runId);
|
|
486
|
-
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
|
+
}
|
|
487
566
|
const unknownPath = await this.#knownStore.slugPath(
|
|
488
567
|
runId,
|
|
489
568
|
"unknown",
|
|
@@ -503,7 +582,12 @@ export default class TurnExecutor {
|
|
|
503
582
|
|
|
504
583
|
const rawTarget = cmd.path || cmd.command || cmd.question || "";
|
|
505
584
|
const target = rawTarget;
|
|
506
|
-
const resultPath = await this.#knownStore.dedup(
|
|
585
|
+
const resultPath = await this.#knownStore.dedup(
|
|
586
|
+
runId,
|
|
587
|
+
scheme,
|
|
588
|
+
target,
|
|
589
|
+
turn,
|
|
590
|
+
);
|
|
507
591
|
|
|
508
592
|
// Pass parsed command fields through as attributes
|
|
509
593
|
const { name: _, ...attributes } = cmd;
|
|
@@ -512,8 +596,69 @@ export default class TurnExecutor {
|
|
|
512
596
|
// known tool or naked write → known:// slug from body
|
|
513
597
|
if (scheme === "known" || (scheme === "set" && !cmd.path)) {
|
|
514
598
|
if (!cmd.body) return null;
|
|
515
|
-
|
|
516
|
-
|
|
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
|
+
}
|
|
517
662
|
await this.#knownStore.upsert(runId, turn, knownPath, cmd.body, 200, {
|
|
518
663
|
loopId,
|
|
519
664
|
});
|
|
@@ -526,20 +671,65 @@ export default class TurnExecutor {
|
|
|
526
671
|
};
|
|
527
672
|
}
|
|
528
673
|
|
|
529
|
-
// Record the entry — 200 OK, handlers change status during dispatch
|
|
530
674
|
const body = cmd.body || cmd.command || cmd.question || "";
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
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
|
+
);
|
|
535
695
|
|
|
536
696
|
return {
|
|
537
|
-
scheme,
|
|
538
|
-
path:
|
|
539
|
-
body,
|
|
540
|
-
attributes,
|
|
697
|
+
scheme: filtered.scheme,
|
|
698
|
+
path: filtered.path,
|
|
699
|
+
body: filtered.body,
|
|
700
|
+
attributes: filtered.attributes,
|
|
541
701
|
status: 200,
|
|
542
|
-
resultPath,
|
|
702
|
+
resultPath: filtered.path,
|
|
543
703
|
};
|
|
544
704
|
}
|
|
705
|
+
|
|
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
|
+
}
|
|
734
|
+
}
|
|
545
735
|
}
|