@possumtech/rummy 0.3.0 → 0.3.1
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 +2 -1
- package/PLUGINS.md +1 -1
- package/SPEC.md +181 -38
- package/migrations/001_initial_schema.sql +1 -1
- package/package.json +7 -3
- package/service.js +5 -3
- package/src/agent/AgentLoop.js +182 -136
- package/src/agent/ContextAssembler.js +2 -0
- package/src/agent/KnownStore.js +28 -85
- package/src/agent/ResponseHealer.js +65 -31
- package/src/agent/TurnExecutor.js +326 -181
- package/src/agent/XmlParser.js +5 -2
- package/src/agent/known_store.sql +48 -0
- package/src/agent/tokens.js +1 -0
- package/src/agent/turns.sql +5 -0
- package/src/hooks/HookRegistry.js +7 -0
- package/src/hooks/Hooks.js +1 -4
- package/src/hooks/ToolRegistry.js +2 -8
- package/src/plugins/budget/README.md +2 -14
- package/src/plugins/budget/budget.js +15 -39
- package/src/plugins/cp/cp.js +1 -1
- package/src/plugins/cp/cpDoc.js +1 -1
- package/src/plugins/get/get.js +71 -1
- package/src/plugins/get/getDoc.js +14 -4
- package/src/plugins/hedberg/matcher.js +10 -29
- package/src/plugins/instructions/preamble.md +16 -6
- package/src/plugins/known/known.js +4 -10
- package/src/plugins/known/knownDoc.js +15 -14
- package/src/plugins/mv/mv.js +18 -1
- package/src/plugins/mv/mvDoc.js +15 -1
- package/src/plugins/{current → performed}/README.md +4 -3
- package/src/plugins/{current/current.js → performed/performed.js} +15 -20
- package/src/plugins/previous/README.md +2 -1
- package/src/plugins/previous/previous.js +31 -25
- package/src/plugins/progress/README.md +1 -2
- package/src/plugins/progress/progress.js +15 -29
- package/src/plugins/prompt/prompt.js +0 -7
- package/src/plugins/rm/rm.js +27 -15
- package/src/plugins/rm/rmDoc.js +3 -3
- package/src/plugins/set/set.js +55 -19
- package/src/plugins/set/setDoc.js +6 -2
- package/src/plugins/telemetry/telemetry.js +14 -9
- package/src/plugins/unknown/README.md +2 -1
- package/src/plugins/unknown/unknown.js +5 -4
- package/src/server/ClientConnection.js +59 -45
- package/src/sql/v_model_context.sql +3 -13
- package/src/plugins/budget/BudgetGuard.js +0 -74
|
@@ -19,6 +19,68 @@ export default class TurnExecutor {
|
|
|
19
19
|
this.#knownStore = knownStore;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Rebuild turn_context from v_model_context, then assemble messages.
|
|
24
|
+
* Called at turn start and again after any fidelity demotion within the turn.
|
|
25
|
+
*/
|
|
26
|
+
async #materializeTurnContext({
|
|
27
|
+
runId,
|
|
28
|
+
loopId,
|
|
29
|
+
turn,
|
|
30
|
+
systemPrompt,
|
|
31
|
+
mode,
|
|
32
|
+
toolSet,
|
|
33
|
+
contextSize,
|
|
34
|
+
demoted,
|
|
35
|
+
}) {
|
|
36
|
+
await this.#db.clear_turn_context.run({ run_id: runId, turn });
|
|
37
|
+
const viewRows = await this.#db.get_model_context.all({ run_id: runId });
|
|
38
|
+
for (const row of viewRows) {
|
|
39
|
+
const scheme = row.scheme || "file";
|
|
40
|
+
const projectedBody = await this.#hooks.tools.view(scheme, {
|
|
41
|
+
path: row.path,
|
|
42
|
+
scheme,
|
|
43
|
+
body: row.body,
|
|
44
|
+
attributes: row.attributes ? JSON.parse(row.attributes) : null,
|
|
45
|
+
fidelity: row.fidelity,
|
|
46
|
+
category: row.category,
|
|
47
|
+
});
|
|
48
|
+
await this.#db.insert_turn_context.run({
|
|
49
|
+
run_id: runId,
|
|
50
|
+
loop_id: loopId,
|
|
51
|
+
turn,
|
|
52
|
+
ordinal: row.ordinal,
|
|
53
|
+
path: row.path,
|
|
54
|
+
fidelity: row.fidelity,
|
|
55
|
+
status: row.status,
|
|
56
|
+
body: projectedBody ?? "",
|
|
57
|
+
tokens: countTokens(projectedBody ?? ""),
|
|
58
|
+
attributes: row.attributes,
|
|
59
|
+
category: row.category,
|
|
60
|
+
source_turn: row.turn,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
const rows = await this.#db.get_turn_context.all({ run_id: runId, turn });
|
|
64
|
+
const lastCtx = await this.#db.get_last_context_tokens.get({
|
|
65
|
+
run_id: runId,
|
|
66
|
+
});
|
|
67
|
+
const lastContextTokens = lastCtx?.context_tokens ?? 0;
|
|
68
|
+
const messages = await ContextAssembler.assembleFromTurnContext(
|
|
69
|
+
rows,
|
|
70
|
+
{
|
|
71
|
+
type: mode,
|
|
72
|
+
systemPrompt,
|
|
73
|
+
contextSize,
|
|
74
|
+
demoted,
|
|
75
|
+
toolSet,
|
|
76
|
+
lastContextTokens,
|
|
77
|
+
turn,
|
|
78
|
+
},
|
|
79
|
+
this.#hooks,
|
|
80
|
+
);
|
|
81
|
+
return { rows, messages, lastContextTokens };
|
|
82
|
+
}
|
|
83
|
+
|
|
22
84
|
async execute({
|
|
23
85
|
mode,
|
|
24
86
|
project,
|
|
@@ -28,12 +90,25 @@ export default class TurnExecutor {
|
|
|
28
90
|
currentLoopId,
|
|
29
91
|
requestedModel,
|
|
30
92
|
loopPrompt,
|
|
93
|
+
loopIteration,
|
|
31
94
|
noRepo,
|
|
32
95
|
toolSet,
|
|
96
|
+
inRecovery = false,
|
|
33
97
|
contextSize,
|
|
34
98
|
options,
|
|
35
99
|
signal,
|
|
36
100
|
}) {
|
|
101
|
+
const RECOVERY_EXCLUDED = new Set([
|
|
102
|
+
"sh",
|
|
103
|
+
"env",
|
|
104
|
+
"search",
|
|
105
|
+
"ask_user",
|
|
106
|
+
"set",
|
|
107
|
+
]);
|
|
108
|
+
const effectiveToolSet = inRecovery
|
|
109
|
+
? new Set([...toolSet].filter((t) => !RECOVERY_EXCLUDED.has(t)))
|
|
110
|
+
: toolSet;
|
|
111
|
+
|
|
37
112
|
const turn = await this.#knownStore.nextTurn(currentRunId);
|
|
38
113
|
|
|
39
114
|
const turnRow = await this.#db.create_turn.get({
|
|
@@ -73,7 +148,7 @@ export default class TurnExecutor {
|
|
|
73
148
|
loopId: currentLoopId,
|
|
74
149
|
turnId: turnRow.id,
|
|
75
150
|
noRepo,
|
|
76
|
-
toolSet,
|
|
151
|
+
toolSet: effectiveToolSet,
|
|
77
152
|
contextSize,
|
|
78
153
|
systemPrompt: null,
|
|
79
154
|
loopPrompt,
|
|
@@ -85,6 +160,7 @@ export default class TurnExecutor {
|
|
|
85
160
|
mode,
|
|
86
161
|
prompt: loopPrompt,
|
|
87
162
|
isContinuation: options?.isContinuation,
|
|
163
|
+
loopIteration,
|
|
88
164
|
});
|
|
89
165
|
|
|
90
166
|
await this.#hooks.processTurn(rummy);
|
|
@@ -111,43 +187,23 @@ export default class TurnExecutor {
|
|
|
111
187
|
});
|
|
112
188
|
|
|
113
189
|
// Materialize turn_context: VIEW rows projected through tools
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
const scheme = row.scheme || "file";
|
|
120
|
-
const projectedBody = await this.#hooks.tools.view(scheme, {
|
|
121
|
-
path: row.path,
|
|
122
|
-
scheme,
|
|
123
|
-
body: row.body,
|
|
124
|
-
attributes: row.attributes ? JSON.parse(row.attributes) : null,
|
|
125
|
-
fidelity: row.fidelity,
|
|
126
|
-
category: row.category,
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
await this.#db.insert_turn_context.run({
|
|
130
|
-
run_id: currentRunId,
|
|
131
|
-
loop_id: currentLoopId,
|
|
190
|
+
const demoted = [];
|
|
191
|
+
let { rows, messages, lastContextTokens } =
|
|
192
|
+
await this.#materializeTurnContext({
|
|
193
|
+
runId: currentRunId,
|
|
194
|
+
loopId: currentLoopId,
|
|
132
195
|
turn,
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
tokens: countTokens(projectedBody ?? ""),
|
|
139
|
-
attributes: row.attributes,
|
|
140
|
-
category: row.category,
|
|
141
|
-
source_turn: row.turn,
|
|
196
|
+
systemPrompt,
|
|
197
|
+
mode,
|
|
198
|
+
toolSet: effectiveToolSet,
|
|
199
|
+
contextSize,
|
|
200
|
+
demoted,
|
|
142
201
|
});
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
const demoted = [];
|
|
146
202
|
|
|
147
203
|
await this.#hooks.context.materialized.emit({
|
|
148
204
|
runId: currentRunId,
|
|
149
205
|
turn,
|
|
150
|
-
rowCount:
|
|
206
|
+
rowCount: rows.length,
|
|
151
207
|
});
|
|
152
208
|
|
|
153
209
|
await this.#hooks.run.progress.emit({
|
|
@@ -157,50 +213,75 @@ export default class TurnExecutor {
|
|
|
157
213
|
status: "thinking",
|
|
158
214
|
});
|
|
159
215
|
|
|
160
|
-
let rows = await this.#db.get_turn_context.all({
|
|
161
|
-
run_id: currentRunId,
|
|
162
|
-
turn,
|
|
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
|
-
|
|
169
|
-
let messages = await ContextAssembler.assembleFromTurnContext(
|
|
170
|
-
rows,
|
|
171
|
-
{
|
|
172
|
-
type: mode,
|
|
173
|
-
systemPrompt,
|
|
174
|
-
contextSize,
|
|
175
|
-
demoted,
|
|
176
|
-
toolSet,
|
|
177
|
-
lastContextTokens,
|
|
178
|
-
},
|
|
179
|
-
this.#hooks,
|
|
180
|
-
);
|
|
181
|
-
|
|
182
216
|
const budgetResult = await this.#hooks.budget.enforce({
|
|
183
217
|
contextSize,
|
|
184
218
|
messages,
|
|
185
219
|
rows,
|
|
220
|
+
lastPromptTokens: lastContextTokens,
|
|
186
221
|
});
|
|
187
222
|
messages = budgetResult.messages;
|
|
188
223
|
rows = budgetResult.rows;
|
|
189
|
-
|
|
224
|
+
let assembledTokens =
|
|
190
225
|
budgetResult.assembledTokens ??
|
|
191
226
|
messages.reduce((sum, m) => sum + countTokens(m.content), 0);
|
|
192
227
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
228
|
+
if (budgetResult.status === 413) {
|
|
229
|
+
if (loopIteration === 1) {
|
|
230
|
+
// Prompt Demotion: first-turn overflow — demote incoming prompt to summary
|
|
231
|
+
const promptRow = rows.findLast(
|
|
232
|
+
(r) => r.category === "prompt" && r.scheme === "prompt",
|
|
233
|
+
);
|
|
234
|
+
if (promptRow) {
|
|
235
|
+
await this.#knownStore.setFidelity(
|
|
236
|
+
currentRunId,
|
|
237
|
+
promptRow.path,
|
|
238
|
+
"summary",
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
const reMat = await this.#materializeTurnContext({
|
|
242
|
+
runId: currentRunId,
|
|
243
|
+
loopId: currentLoopId,
|
|
244
|
+
turn,
|
|
245
|
+
systemPrompt,
|
|
246
|
+
mode,
|
|
247
|
+
toolSet: effectiveToolSet,
|
|
248
|
+
contextSize,
|
|
249
|
+
demoted,
|
|
250
|
+
});
|
|
251
|
+
rows = reMat.rows;
|
|
252
|
+
messages = reMat.messages;
|
|
253
|
+
const recheck = await this.#hooks.budget.enforce({
|
|
254
|
+
contextSize,
|
|
255
|
+
messages,
|
|
256
|
+
rows,
|
|
257
|
+
lastPromptTokens: reMat.lastContextTokens,
|
|
258
|
+
});
|
|
259
|
+
messages = recheck.messages;
|
|
260
|
+
rows = recheck.rows;
|
|
261
|
+
assembledTokens =
|
|
262
|
+
recheck.assembledTokens ??
|
|
263
|
+
messages.reduce((sum, m) => sum + countTokens(m.content), 0);
|
|
264
|
+
if (recheck.status === 413) {
|
|
265
|
+
return {
|
|
266
|
+
turn,
|
|
267
|
+
turnId: turnRow.id,
|
|
268
|
+
status: 413,
|
|
269
|
+
assembledTokens,
|
|
270
|
+
contextSize,
|
|
271
|
+
overflow: recheck.overflow,
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
} else {
|
|
275
|
+
// Base context too large even without new prompt — genuine failure
|
|
276
|
+
return {
|
|
277
|
+
turn,
|
|
278
|
+
turnId: turnRow.id,
|
|
279
|
+
status: 413,
|
|
280
|
+
assembledTokens,
|
|
281
|
+
contextSize,
|
|
282
|
+
overflow: budgetResult.overflow,
|
|
283
|
+
};
|
|
284
|
+
}
|
|
204
285
|
}
|
|
205
286
|
|
|
206
287
|
const filteredMessages = await this.#hooks.llm.messages.filter(messages, {
|
|
@@ -318,104 +399,164 @@ export default class TurnExecutor {
|
|
|
318
399
|
}
|
|
319
400
|
|
|
320
401
|
// --- 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;
|
|
328
|
-
|
|
329
402
|
let hasErrors = false;
|
|
330
403
|
let hasProposed = false;
|
|
331
404
|
let abortAfter = null;
|
|
332
405
|
const dispatched = [...lifecycle];
|
|
333
406
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
407
|
+
// Lifecycle signals first — always dispatched, never aborted.
|
|
408
|
+
for (const entry of lifecycle) {
|
|
409
|
+
await this.#hooks.tool.before.emit({ entry, rummy });
|
|
410
|
+
await this.#hooks.tools.dispatch(entry.scheme, entry, rummy);
|
|
411
|
+
await this.#hooks.tool.after.emit({ entry, rummy });
|
|
412
|
+
await this.#hooks.entry.created.emit(entry);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
for (const entry of actions) {
|
|
416
|
+
if (abortAfter) {
|
|
417
|
+
const errorMsg = `Aborted — preceding <${abortAfter}> requires resolution.`;
|
|
418
|
+
await this.#knownStore.upsert(
|
|
419
|
+
currentRunId,
|
|
420
|
+
turn,
|
|
421
|
+
entry.resultPath || entry.path,
|
|
422
|
+
errorMsg,
|
|
423
|
+
409,
|
|
424
|
+
{ attributes: { error: errorMsg }, loopId: currentLoopId },
|
|
425
|
+
);
|
|
426
|
+
hasErrors = true;
|
|
427
|
+
continue;
|
|
341
428
|
}
|
|
342
429
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
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
|
-
}
|
|
430
|
+
await this.#hooks.tool.before.emit({ entry, rummy });
|
|
431
|
+
await this.#hooks.tools.dispatch(entry.scheme, entry, rummy);
|
|
432
|
+
await this.#hooks.tool.after.emit({ entry, rummy });
|
|
433
|
+
await this.#hooks.entry.created.emit(entry);
|
|
434
|
+
dispatched.push(entry);
|
|
363
435
|
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
hasErrors = true;
|
|
382
|
-
abortAfter = entry.scheme;
|
|
383
|
-
continue;
|
|
384
|
-
}
|
|
385
|
-
throw err;
|
|
386
|
-
}
|
|
436
|
+
const row = await this.#db.get_entry_state.get({
|
|
437
|
+
run_id: currentRunId,
|
|
438
|
+
path: entry.resultPath || entry.path,
|
|
439
|
+
});
|
|
440
|
+
if (row?.status === 202) {
|
|
441
|
+
hasProposed = true;
|
|
442
|
+
abortAfter = entry.scheme;
|
|
443
|
+
} else if (row?.status >= 400) {
|
|
444
|
+
hasErrors = true;
|
|
445
|
+
abortAfter = entry.scheme;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// Materialize proposals only if we dispatched actions
|
|
450
|
+
if (!abortAfter || hasProposed) {
|
|
451
|
+
await this.#hooks.turn.proposing.emit({ rummy, recorded: dispatched });
|
|
452
|
+
}
|
|
387
453
|
|
|
454
|
+
// Recheck after materialization (set handler may create proposals)
|
|
455
|
+
if (!hasProposed && !hasErrors) {
|
|
456
|
+
for (const entry of actions) {
|
|
388
457
|
const row = await this.#db.get_entry_state.get({
|
|
389
458
|
run_id: currentRunId,
|
|
390
459
|
path: entry.resultPath || entry.path,
|
|
391
460
|
});
|
|
392
|
-
if (row?.status === 202)
|
|
393
|
-
|
|
394
|
-
abortAfter = entry.scheme;
|
|
395
|
-
} else if (row?.status >= 400) {
|
|
396
|
-
hasErrors = true;
|
|
397
|
-
abortAfter = entry.scheme;
|
|
398
|
-
}
|
|
461
|
+
if (row?.status === 202) hasProposed = true;
|
|
462
|
+
if (row?.status >= 400) hasErrors = true;
|
|
399
463
|
}
|
|
464
|
+
}
|
|
400
465
|
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
466
|
+
// Turn Demotion: if end-of-turn context exceeds ceiling, demote this
|
|
467
|
+
// turn's data entries and the incoming prompt to summary, then force a
|
|
468
|
+
// budget recovery phase before continuing.
|
|
469
|
+
let budgetRecovery = null;
|
|
470
|
+
// Use actual prompt_tokens from this turn's LLM response as the ground-truth
|
|
471
|
+
// token count for post-turn budget checks — more accurate than the estimate.
|
|
472
|
+
const currentPromptTokens = result.usage?.prompt_tokens ?? 0;
|
|
473
|
+
if (contextSize) {
|
|
474
|
+
const postMat = await this.#materializeTurnContext({
|
|
475
|
+
runId: currentRunId,
|
|
476
|
+
loopId: currentLoopId,
|
|
477
|
+
turn,
|
|
478
|
+
systemPrompt,
|
|
479
|
+
mode,
|
|
480
|
+
toolSet: effectiveToolSet,
|
|
481
|
+
contextSize,
|
|
482
|
+
demoted,
|
|
483
|
+
});
|
|
484
|
+
const postBudget = await this.#hooks.budget.enforce({
|
|
485
|
+
contextSize,
|
|
486
|
+
messages: postMat.messages,
|
|
487
|
+
rows: postMat.rows,
|
|
488
|
+
lastPromptTokens: currentPromptTokens,
|
|
489
|
+
});
|
|
490
|
+
if (postBudget.status === 413) {
|
|
491
|
+
// Demote this turn's data entries.
|
|
492
|
+
const demotedEntries = await this.#db.demote_turn_data_entries.all({
|
|
493
|
+
run_id: currentRunId,
|
|
494
|
+
turn,
|
|
495
|
+
});
|
|
496
|
+
const paths = demotedEntries.map((r) => r.path).join(", ");
|
|
405
497
|
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
if (row?.status >= 400) hasErrors = true;
|
|
498
|
+
// Also summarize the prompt — forces the model to earn it back.
|
|
499
|
+
const promptRow = postMat.rows.find((r) => r.scheme === "prompt");
|
|
500
|
+
if (promptRow) {
|
|
501
|
+
await this.#knownStore.setFidelity(
|
|
502
|
+
currentRunId,
|
|
503
|
+
promptRow.path,
|
|
504
|
+
"summary",
|
|
505
|
+
);
|
|
415
506
|
}
|
|
507
|
+
|
|
508
|
+
// Re-materialize after both demotions for accurate token count.
|
|
509
|
+
const recoveryMat = await this.#materializeTurnContext({
|
|
510
|
+
runId: currentRunId,
|
|
511
|
+
loopId: currentLoopId,
|
|
512
|
+
turn,
|
|
513
|
+
systemPrompt,
|
|
514
|
+
mode,
|
|
515
|
+
toolSet: effectiveToolSet,
|
|
516
|
+
contextSize,
|
|
517
|
+
demoted,
|
|
518
|
+
});
|
|
519
|
+
const recoveryBudget = await this.#hooks.budget.enforce({
|
|
520
|
+
contextSize,
|
|
521
|
+
messages: recoveryMat.messages,
|
|
522
|
+
rows: recoveryMat.rows,
|
|
523
|
+
lastPromptTokens: currentPromptTokens,
|
|
524
|
+
});
|
|
525
|
+
const safeLevel = Math.floor(contextSize * 0.9);
|
|
526
|
+
const tokensToFree = Math.max(
|
|
527
|
+
0,
|
|
528
|
+
recoveryBudget.assembledTokens - safeLevel,
|
|
529
|
+
);
|
|
530
|
+
|
|
531
|
+
const promptLine =
|
|
532
|
+
tokensToFree > 0
|
|
533
|
+
? `Info: Prompt auto-summarized. Full prompt restores automatically when you free ${tokensToFree} tokens.`
|
|
534
|
+
: "Info: Prompt auto-summarized. It will restore automatically.";
|
|
535
|
+
const body = [
|
|
536
|
+
"Error 413: Context Size Exceeded",
|
|
537
|
+
"",
|
|
538
|
+
"Required: YOU MUST demote larger and/or less relevant items to optimize your context.",
|
|
539
|
+
`Info: ${paths} have been automatically summarized to avoid overflow.`,
|
|
540
|
+
promptLine,
|
|
541
|
+
"Info: YOU MAY use bulk patterns to demote and promote entries by pattern.",
|
|
542
|
+
"Info: Well-designed paths and summaries improve context management.",
|
|
543
|
+
'Example: <set path="known://people/*" fidelity="summary"/>',
|
|
544
|
+
].join("\n");
|
|
545
|
+
|
|
546
|
+
await this.#knownStore.upsert(
|
|
547
|
+
currentRunId,
|
|
548
|
+
turn,
|
|
549
|
+
`budget://${currentLoopId}/${turn}`,
|
|
550
|
+
body,
|
|
551
|
+
413,
|
|
552
|
+
{ loopId: currentLoopId },
|
|
553
|
+
);
|
|
554
|
+
|
|
555
|
+
budgetRecovery = {
|
|
556
|
+
target: safeLevel,
|
|
557
|
+
promptPath: promptRow?.path ?? null,
|
|
558
|
+
};
|
|
416
559
|
}
|
|
417
|
-
} finally {
|
|
418
|
-
this.#hooks.budget.deactivate(this.#knownStore);
|
|
419
560
|
}
|
|
420
561
|
|
|
421
562
|
// Lifecycle signals are always available — never 409'd.
|
|
@@ -433,6 +574,15 @@ export default class TurnExecutor {
|
|
|
433
574
|
console.warn(
|
|
434
575
|
"[RUMMY] Overriding <summarize> — actions in this turn failed. Continuing.",
|
|
435
576
|
);
|
|
577
|
+
// Mark the recorded summarize entry as 409 so the model sees it was rejected
|
|
578
|
+
if (summaryEntry?.path) {
|
|
579
|
+
await this.#knownStore.resolve(
|
|
580
|
+
currentRunId,
|
|
581
|
+
summaryEntry.path,
|
|
582
|
+
409,
|
|
583
|
+
"Overridden — actions in this turn failed. Use <update/> until resolved.",
|
|
584
|
+
);
|
|
585
|
+
}
|
|
436
586
|
updateText = summaryText;
|
|
437
587
|
summaryText = null;
|
|
438
588
|
}
|
|
@@ -484,12 +634,11 @@ export default class TurnExecutor {
|
|
|
484
634
|
flags,
|
|
485
635
|
model: result.model || requestedModel,
|
|
486
636
|
modelAlias: requestedModel,
|
|
487
|
-
temperature:
|
|
488
|
-
options?.temperature ??
|
|
489
|
-
Number.parseFloat(process.env.RUMMY_TEMPERATURE || "0.7"),
|
|
637
|
+
temperature: options?.temperature,
|
|
490
638
|
contextSize,
|
|
491
639
|
assembledTokens,
|
|
492
640
|
usage: result.usage,
|
|
641
|
+
budgetRecovery,
|
|
493
642
|
};
|
|
494
643
|
|
|
495
644
|
await this.#hooks.turn.completed.emit(turnResult);
|
|
@@ -502,8 +651,7 @@ export default class TurnExecutor {
|
|
|
502
651
|
* Returns the recorded entry descriptor, or null if rejected/skipped.
|
|
503
652
|
*/
|
|
504
653
|
async #record(runId, loopId, turn, mode, cmd) {
|
|
505
|
-
|
|
506
|
-
if (mode === "ask" || mode === "panic") {
|
|
654
|
+
if (mode === "ask") {
|
|
507
655
|
if (cmd.name === "sh") {
|
|
508
656
|
console.warn("[RUMMY] Rejected <sh> in ask mode");
|
|
509
657
|
return null;
|
|
@@ -581,6 +729,31 @@ export default class TurnExecutor {
|
|
|
581
729
|
}
|
|
582
730
|
|
|
583
731
|
const rawTarget = cmd.path || cmd.command || cmd.question || "";
|
|
732
|
+
// Reject paths that are likely reasoning bleed — too long or contain non-printing chars
|
|
733
|
+
if (rawTarget.length > 512 || /\p{Cc}/u.test(rawTarget)) {
|
|
734
|
+
const rejectPath = await this.#knownStore.dedup(
|
|
735
|
+
runId,
|
|
736
|
+
scheme,
|
|
737
|
+
`${scheme}://invalid`,
|
|
738
|
+
turn,
|
|
739
|
+
);
|
|
740
|
+
await this.#knownStore.upsert(
|
|
741
|
+
runId,
|
|
742
|
+
turn,
|
|
743
|
+
rejectPath,
|
|
744
|
+
`Invalid path: too long or contains non-printing characters`,
|
|
745
|
+
400,
|
|
746
|
+
{ loopId },
|
|
747
|
+
);
|
|
748
|
+
return {
|
|
749
|
+
scheme,
|
|
750
|
+
path: rejectPath,
|
|
751
|
+
body: "",
|
|
752
|
+
attributes: {},
|
|
753
|
+
status: 400,
|
|
754
|
+
resultPath: rejectPath,
|
|
755
|
+
};
|
|
756
|
+
}
|
|
584
757
|
const target = rawTarget;
|
|
585
758
|
const resultPath = await this.#knownStore.dedup(
|
|
586
759
|
runId,
|
|
@@ -648,6 +821,7 @@ export default class TurnExecutor {
|
|
|
648
821
|
cmd.body || existing[0].body,
|
|
649
822
|
200,
|
|
650
823
|
{
|
|
824
|
+
attributes,
|
|
651
825
|
loopId,
|
|
652
826
|
},
|
|
653
827
|
);
|
|
@@ -660,6 +834,7 @@ export default class TurnExecutor {
|
|
|
660
834
|
};
|
|
661
835
|
}
|
|
662
836
|
await this.#knownStore.upsert(runId, turn, knownPath, cmd.body, 200, {
|
|
837
|
+
attributes,
|
|
663
838
|
loopId,
|
|
664
839
|
});
|
|
665
840
|
return {
|
|
@@ -702,34 +877,4 @@ export default class TurnExecutor {
|
|
|
702
877
|
resultPath: filtered.path,
|
|
703
878
|
};
|
|
704
879
|
}
|
|
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
|
-
}
|
|
735
880
|
}
|
package/src/agent/XmlParser.js
CHANGED
|
@@ -4,7 +4,7 @@ import { normalizeAttrs, parseJsonEdit } from "../plugins/hedberg/normalize.js";
|
|
|
4
4
|
import { parseSed } from "../plugins/hedberg/sed.js";
|
|
5
5
|
|
|
6
6
|
const STORE_TOOLS = new Set(["get", "rm", "set", "mv", "cp", "search"]);
|
|
7
|
-
const ALL_TOOLS = new Set([
|
|
7
|
+
export const ALL_TOOLS = new Set([
|
|
8
8
|
...STORE_TOOLS,
|
|
9
9
|
"known",
|
|
10
10
|
"sh",
|
|
@@ -13,6 +13,9 @@ const ALL_TOOLS = new Set([
|
|
|
13
13
|
"summarize",
|
|
14
14
|
"update",
|
|
15
15
|
"unknown",
|
|
16
|
+
"think",
|
|
17
|
+
"thought",
|
|
18
|
+
"mcp",
|
|
16
19
|
]);
|
|
17
20
|
|
|
18
21
|
/**
|
|
@@ -100,7 +103,7 @@ function resolveCommand(name, attrs, rawBody) {
|
|
|
100
103
|
if (name === "known") {
|
|
101
104
|
const body = trimmed || a.body || "";
|
|
102
105
|
const path = a.path || null;
|
|
103
|
-
return { name, path, body };
|
|
106
|
+
return { name, ...a, path, body };
|
|
104
107
|
}
|
|
105
108
|
|
|
106
109
|
if (name === "get" || name === "rm") {
|