@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
package/src/agent/AgentLoop.js
CHANGED
|
@@ -32,7 +32,7 @@ export default class AgentLoop {
|
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
async #ensureRun(projectId, model, run, options) {
|
|
35
|
-
const
|
|
35
|
+
const _noRepo = options?.noRepo === true;
|
|
36
36
|
const isFork = options?.fork === true;
|
|
37
37
|
const requestedModel = model;
|
|
38
38
|
|
|
@@ -54,6 +54,11 @@ export default class AgentLoop {
|
|
|
54
54
|
new_run_id: runRow.id,
|
|
55
55
|
parent_run_id: existingRun.id,
|
|
56
56
|
});
|
|
57
|
+
await this.#hooks.run.created.emit({
|
|
58
|
+
runId: runRow.id,
|
|
59
|
+
alias,
|
|
60
|
+
forkedFrom: existingRun.id,
|
|
61
|
+
});
|
|
57
62
|
return { runId: runRow.id, alias };
|
|
58
63
|
}
|
|
59
64
|
|
|
@@ -87,6 +92,7 @@ export default class AgentLoop {
|
|
|
87
92
|
persona: options?.persona ?? null,
|
|
88
93
|
context_limit: options?.contextLimit ?? null,
|
|
89
94
|
});
|
|
95
|
+
await this.#hooks.run.created.emit({ runId: runRow.id, alias });
|
|
90
96
|
return { runId: runRow.id, alias };
|
|
91
97
|
}
|
|
92
98
|
|
|
@@ -112,7 +118,9 @@ export default class AgentLoop {
|
|
|
112
118
|
if (!project)
|
|
113
119
|
throw new Error(msg("error.project_not_found", { projectId }));
|
|
114
120
|
|
|
115
|
-
const
|
|
121
|
+
const noRepo = options?.noRepo === true;
|
|
122
|
+
const noInteraction = options?.noInteraction === true;
|
|
123
|
+
const noWeb = options?.noWeb === true;
|
|
116
124
|
const requestedModel = model;
|
|
117
125
|
|
|
118
126
|
const runInfo = await this.#ensureRun(projectId, model, run, options);
|
|
@@ -134,7 +142,12 @@ export default class AgentLoop {
|
|
|
134
142
|
mode,
|
|
135
143
|
model: requestedModel,
|
|
136
144
|
prompt: prompt || "",
|
|
137
|
-
config: JSON.stringify({
|
|
145
|
+
config: JSON.stringify({
|
|
146
|
+
noRepo,
|
|
147
|
+
noInteraction,
|
|
148
|
+
noWeb,
|
|
149
|
+
temperature: options?.temperature,
|
|
150
|
+
}),
|
|
138
151
|
});
|
|
139
152
|
|
|
140
153
|
if (this.#activeRuns.has(currentRunId)) {
|
|
@@ -151,6 +164,8 @@ export default class AgentLoop {
|
|
|
151
164
|
}
|
|
152
165
|
|
|
153
166
|
async #drainQueue(currentRunId, currentAlias, projectId, project, options) {
|
|
167
|
+
let panicAttempted = false;
|
|
168
|
+
|
|
154
169
|
while (true) {
|
|
155
170
|
const loop = await this.#db.claim_next_loop.get({
|
|
156
171
|
run_id: currentRunId,
|
|
@@ -158,6 +173,13 @@ export default class AgentLoop {
|
|
|
158
173
|
if (!loop) break;
|
|
159
174
|
|
|
160
175
|
const loopConfig = loop.config ? JSON.parse(loop.config) : {};
|
|
176
|
+
const hook =
|
|
177
|
+
loop.mode === "panic"
|
|
178
|
+
? this.#hooks.panic
|
|
179
|
+
: loop.mode === "ask"
|
|
180
|
+
? this.#hooks.ask
|
|
181
|
+
: this.#hooks.act;
|
|
182
|
+
|
|
161
183
|
const result = await this.#executeLoop({
|
|
162
184
|
mode: loop.mode,
|
|
163
185
|
project,
|
|
@@ -167,11 +189,64 @@ export default class AgentLoop {
|
|
|
167
189
|
currentLoopId: loop.id,
|
|
168
190
|
requestedModel: loop.model,
|
|
169
191
|
prompt: loop.prompt,
|
|
170
|
-
|
|
192
|
+
noRepo: loopConfig.noRepo || false,
|
|
193
|
+
noInteraction: loopConfig.noInteraction || false,
|
|
194
|
+
noWeb: loopConfig.noWeb || false,
|
|
171
195
|
options: { ...options, temperature: loopConfig.temperature },
|
|
172
|
-
hook
|
|
196
|
+
hook,
|
|
173
197
|
});
|
|
174
198
|
|
|
199
|
+
if (result.status === 413) {
|
|
200
|
+
await this.#db.complete_loop.run({
|
|
201
|
+
id: loop.id,
|
|
202
|
+
status: 413,
|
|
203
|
+
result: JSON.stringify(result),
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
// One panic attempt per drain cycle
|
|
207
|
+
if (loop.mode === "panic" || panicAttempted) {
|
|
208
|
+
return {
|
|
209
|
+
run: currentAlias,
|
|
210
|
+
status: 413,
|
|
211
|
+
error:
|
|
212
|
+
loop.mode === "panic"
|
|
213
|
+
? `Panic mode failed to free enough space (${result.overflow} tokens over).`
|
|
214
|
+
: `Context full (${result.overflow} tokens over).`,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
panicAttempted = true;
|
|
219
|
+
|
|
220
|
+
const panicPrompt = this.#hooks.budget.panicPrompt({
|
|
221
|
+
assembledTokens: result.assembledTokens,
|
|
222
|
+
contextSize: result.contextSize,
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
// Enqueue panic loop
|
|
226
|
+
const panicSeq = await this.#db.next_loop.get({ run_id: currentRunId });
|
|
227
|
+
await this.#db.enqueue_loop.get({
|
|
228
|
+
run_id: currentRunId,
|
|
229
|
+
sequence: panicSeq.sequence,
|
|
230
|
+
mode: "panic",
|
|
231
|
+
model: loop.model,
|
|
232
|
+
prompt: panicPrompt,
|
|
233
|
+
config: JSON.stringify({ noRepo: true }),
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
// Re-enqueue the original loop to retry after panic
|
|
237
|
+
const retrySeq = await this.#db.next_loop.get({ run_id: currentRunId });
|
|
238
|
+
await this.#db.enqueue_loop.get({
|
|
239
|
+
run_id: currentRunId,
|
|
240
|
+
sequence: retrySeq.sequence,
|
|
241
|
+
mode: loop.mode,
|
|
242
|
+
model: loop.model,
|
|
243
|
+
prompt: loop.prompt,
|
|
244
|
+
config: loop.config,
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
|
|
175
250
|
await this.#db.complete_loop.run({
|
|
176
251
|
id: loop.id,
|
|
177
252
|
status: result.status === 202 ? 202 : result.status,
|
|
@@ -194,7 +269,9 @@ export default class AgentLoop {
|
|
|
194
269
|
currentLoopId,
|
|
195
270
|
requestedModel,
|
|
196
271
|
prompt,
|
|
197
|
-
|
|
272
|
+
noRepo,
|
|
273
|
+
noInteraction,
|
|
274
|
+
noWeb,
|
|
198
275
|
options,
|
|
199
276
|
hook,
|
|
200
277
|
}) {
|
|
@@ -212,6 +289,11 @@ export default class AgentLoop {
|
|
|
212
289
|
? Math.min(runRow.context_limit, modelContextSize)
|
|
213
290
|
: modelContextSize;
|
|
214
291
|
|
|
292
|
+
const toolSet = this.#hooks.tools.resolveForLoop(mode, {
|
|
293
|
+
noInteraction,
|
|
294
|
+
noWeb,
|
|
295
|
+
});
|
|
296
|
+
|
|
215
297
|
let loopIteration = 0;
|
|
216
298
|
const MAX_LOOP_ITERATIONS = Number(process.env.RUMMY_MAX_TURNS) || 15;
|
|
217
299
|
const healer = new ResponseHealer();
|
|
@@ -219,6 +301,17 @@ export default class AgentLoop {
|
|
|
219
301
|
const controller = new AbortController();
|
|
220
302
|
this.#activeRuns.set(currentRunId, controller);
|
|
221
303
|
|
|
304
|
+
let _lastAssembledTokens = 0;
|
|
305
|
+
let _panicStrikes = 0;
|
|
306
|
+
let _lastPanicTokens = null;
|
|
307
|
+
|
|
308
|
+
await this.#hooks.loop.started.emit({
|
|
309
|
+
runId: currentRunId,
|
|
310
|
+
loopId: currentLoopId,
|
|
311
|
+
mode,
|
|
312
|
+
prompt,
|
|
313
|
+
});
|
|
314
|
+
|
|
222
315
|
try {
|
|
223
316
|
while (loopIteration < MAX_LOOP_ITERATIONS) {
|
|
224
317
|
if (controller.signal.aborted) {
|
|
@@ -239,6 +332,8 @@ export default class AgentLoop {
|
|
|
239
332
|
let turnPrompt;
|
|
240
333
|
if (loopIteration === 1) {
|
|
241
334
|
turnPrompt = prompt;
|
|
335
|
+
} else if (mode === "panic") {
|
|
336
|
+
turnPrompt = "Continue freeing space. Check <knowns> token counts.";
|
|
242
337
|
} else {
|
|
243
338
|
turnPrompt = this.#buildContinuationPrompt(
|
|
244
339
|
loopIteration,
|
|
@@ -255,12 +350,67 @@ export default class AgentLoop {
|
|
|
255
350
|
currentLoopId,
|
|
256
351
|
requestedModel,
|
|
257
352
|
loopPrompt: turnPrompt,
|
|
258
|
-
|
|
353
|
+
noRepo,
|
|
354
|
+
toolSet,
|
|
259
355
|
contextSize,
|
|
260
356
|
options: { ...options, isContinuation: loopIteration > 1 },
|
|
261
357
|
signal: controller.signal,
|
|
262
358
|
});
|
|
263
359
|
|
|
360
|
+
// Budget overflow — return 413 to drainQueue for panic mode
|
|
361
|
+
if (result.status === 413) {
|
|
362
|
+
return {
|
|
363
|
+
run: currentAlias,
|
|
364
|
+
status: 413,
|
|
365
|
+
overflow: result.overflow,
|
|
366
|
+
assembledTokens: result.assembledTokens,
|
|
367
|
+
contextSize: result.contextSize,
|
|
368
|
+
turn: result.turn,
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
_lastAssembledTokens = result.assembledTokens;
|
|
373
|
+
|
|
374
|
+
// Panic mode: target check + strike counting
|
|
375
|
+
if (mode === "panic") {
|
|
376
|
+
const panicTarget = Math.floor(contextSize * 0.75);
|
|
377
|
+
if (result.assembledTokens <= panicTarget) {
|
|
378
|
+
await this.#db.update_run_status.run({
|
|
379
|
+
id: currentRunId,
|
|
380
|
+
status: 200,
|
|
381
|
+
});
|
|
382
|
+
const out = {
|
|
383
|
+
run: currentAlias,
|
|
384
|
+
status: 200,
|
|
385
|
+
turn: result.turn,
|
|
386
|
+
};
|
|
387
|
+
await hook.completed.emit({ projectId, ...out });
|
|
388
|
+
return out;
|
|
389
|
+
}
|
|
390
|
+
if (_lastPanicTokens !== null) {
|
|
391
|
+
if (result.assembledTokens < _lastPanicTokens) {
|
|
392
|
+
_panicStrikes = 0;
|
|
393
|
+
} else {
|
|
394
|
+
_panicStrikes++;
|
|
395
|
+
if (_panicStrikes >= 3) {
|
|
396
|
+
await this.#db.update_run_status.run({
|
|
397
|
+
id: currentRunId,
|
|
398
|
+
status: 200,
|
|
399
|
+
});
|
|
400
|
+
return {
|
|
401
|
+
run: currentAlias,
|
|
402
|
+
status: 413,
|
|
403
|
+
overflow: result.assembledTokens - contextSize,
|
|
404
|
+
assembledTokens: result.assembledTokens,
|
|
405
|
+
contextSize,
|
|
406
|
+
turn: result.turn,
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
_lastPanicTokens = result.assembledTokens;
|
|
412
|
+
}
|
|
413
|
+
|
|
264
414
|
const runUsage = await this.#db.get_run_usage.get({
|
|
265
415
|
run_id: currentRunId,
|
|
266
416
|
});
|
|
@@ -409,6 +559,14 @@ export default class AgentLoop {
|
|
|
409
559
|
return out;
|
|
410
560
|
} finally {
|
|
411
561
|
this.#activeRuns.delete(currentRunId);
|
|
562
|
+
await this.#hooks.loop.completed
|
|
563
|
+
.emit({
|
|
564
|
+
runId: currentRunId,
|
|
565
|
+
loopId: currentLoopId,
|
|
566
|
+
mode,
|
|
567
|
+
turns: loopIteration,
|
|
568
|
+
})
|
|
569
|
+
.catch(() => {});
|
|
412
570
|
}
|
|
413
571
|
}
|
|
414
572
|
|
|
@@ -546,16 +704,9 @@ export default class AgentLoop {
|
|
|
546
704
|
runRow.id,
|
|
547
705
|
nextTurn,
|
|
548
706
|
`prompt://${nextTurn}`,
|
|
549
|
-
"",
|
|
550
|
-
200,
|
|
551
|
-
{ attributes: { mode: "ask" } },
|
|
552
|
-
);
|
|
553
|
-
await this.#knownStore.upsert(
|
|
554
|
-
runRow.id,
|
|
555
|
-
nextTurn,
|
|
556
|
-
`ask://${nextTurn}`,
|
|
557
707
|
message,
|
|
558
708
|
200,
|
|
709
|
+
{ attributes: { mode: "ask" } },
|
|
559
710
|
);
|
|
560
711
|
|
|
561
712
|
if (this.#activeRuns.has(runRow.id)) {
|
|
@@ -6,17 +6,31 @@
|
|
|
6
6
|
export default class ContextAssembler {
|
|
7
7
|
static async assembleFromTurnContext(
|
|
8
8
|
rows,
|
|
9
|
-
{
|
|
9
|
+
{
|
|
10
|
+
type = "ask",
|
|
11
|
+
systemPrompt = "",
|
|
12
|
+
contextSize = 0,
|
|
13
|
+
demoted = [],
|
|
14
|
+
toolSet = null,
|
|
15
|
+
lastContextTokens = 0,
|
|
16
|
+
} = {},
|
|
10
17
|
hooks,
|
|
11
18
|
) {
|
|
12
19
|
// Find loop boundary from active prompt
|
|
13
20
|
const promptEntry = rows.findLast(
|
|
14
|
-
(r) =>
|
|
15
|
-
r.category === "prompt" && (r.scheme === "ask" || r.scheme === "act"),
|
|
21
|
+
(r) => r.category === "prompt" && r.scheme === "prompt",
|
|
16
22
|
);
|
|
17
23
|
const loopStartTurn = promptEntry?.source_turn ?? 0;
|
|
18
24
|
|
|
19
|
-
const ctx = {
|
|
25
|
+
const ctx = {
|
|
26
|
+
rows,
|
|
27
|
+
loopStartTurn,
|
|
28
|
+
type,
|
|
29
|
+
contextSize,
|
|
30
|
+
lastContextTokens,
|
|
31
|
+
demoted,
|
|
32
|
+
toolSet,
|
|
33
|
+
};
|
|
20
34
|
|
|
21
35
|
const system = await hooks.assembly.system.filter(systemPrompt, ctx);
|
|
22
36
|
const user = await hooks.assembly.user.filter("", ctx);
|
package/src/agent/KnownStore.js
CHANGED
|
@@ -1,10 +1,42 @@
|
|
|
1
1
|
import slugify from "../sql/functions/slugify.js";
|
|
2
|
+
import { countTokens } from "./tokens.js";
|
|
2
3
|
|
|
3
4
|
export default class KnownStore {
|
|
4
5
|
#db;
|
|
6
|
+
#onChanged;
|
|
7
|
+
#budgetGuard = null;
|
|
8
|
+
#schemes = new Map();
|
|
5
9
|
|
|
6
|
-
constructor(db) {
|
|
10
|
+
constructor(db, { onChanged } = {}) {
|
|
7
11
|
this.#db = db;
|
|
12
|
+
this.#onChanged = onChanged || null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
get budgetGuard() {
|
|
16
|
+
return this.#budgetGuard;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
set budgetGuard(guard) {
|
|
20
|
+
this.#budgetGuard = guard;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async loadSchemes(db) {
|
|
24
|
+
const rows = await (db || this.#db).get_all_schemes.all();
|
|
25
|
+
this.#schemes.clear();
|
|
26
|
+
for (const row of rows) {
|
|
27
|
+
this.#schemes.set(row.name, row);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
#isVisible(path, fidelity) {
|
|
32
|
+
if (fidelity === "archive") return false;
|
|
33
|
+
const scheme = KnownStore.scheme(path) ?? "file";
|
|
34
|
+
const meta = this.#schemes.get(scheme);
|
|
35
|
+
return meta ? meta.model_visible !== 0 : true;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
#emitChanged(runId, path, changeType) {
|
|
39
|
+
if (this.#onChanged) this.#onChanged({ runId, path, changeType });
|
|
8
40
|
}
|
|
9
41
|
|
|
10
42
|
static scheme(path) {
|
|
@@ -30,18 +62,21 @@ export default class KnownStore {
|
|
|
30
62
|
return row.turn;
|
|
31
63
|
}
|
|
32
64
|
|
|
33
|
-
async dedup(runId, scheme, target) {
|
|
34
|
-
const
|
|
65
|
+
async dedup(runId, scheme, target, turn) {
|
|
66
|
+
const encodedTarget = encodeURIComponent(target);
|
|
67
|
+
const turnPrefix = turn ? `turn_${turn}/` : "";
|
|
68
|
+
const candidate = `${scheme}://${turnPrefix}${encodedTarget}`;
|
|
35
69
|
const existing = await this.#db.get_entry_body.get({
|
|
36
70
|
run_id: runId,
|
|
37
|
-
path:
|
|
71
|
+
path: candidate,
|
|
38
72
|
});
|
|
39
73
|
if (!existing) return candidate;
|
|
40
74
|
return `${candidate}_${Date.now()}`;
|
|
41
75
|
}
|
|
42
76
|
|
|
43
|
-
async slugPath(runId, scheme, content) {
|
|
44
|
-
const
|
|
77
|
+
async slugPath(runId, scheme, content, summary) {
|
|
78
|
+
const source = summary ? summary.replace(/,\s*/g, "/") : content || "";
|
|
79
|
+
const base = slugify(source);
|
|
45
80
|
const prefix = `${scheme}://`;
|
|
46
81
|
|
|
47
82
|
if (!base) return `${prefix}${Date.now()}`;
|
|
@@ -70,11 +105,28 @@ export default class KnownStore {
|
|
|
70
105
|
loopId = null,
|
|
71
106
|
} = {},
|
|
72
107
|
) {
|
|
108
|
+
const normalized = KnownStore.normalizePath(path);
|
|
109
|
+
let delta = 0;
|
|
110
|
+
|
|
111
|
+
if (
|
|
112
|
+
this.#budgetGuard &&
|
|
113
|
+
status < 400 &&
|
|
114
|
+
this.#isVisible(normalized, fidelity)
|
|
115
|
+
) {
|
|
116
|
+
const existing = await this.#db.get_entry_body.get({
|
|
117
|
+
run_id: runId,
|
|
118
|
+
path: normalized,
|
|
119
|
+
});
|
|
120
|
+
delta =
|
|
121
|
+
countTokens(body) - (existing?.body ? countTokens(existing.body) : 0);
|
|
122
|
+
this.#budgetGuard.check(delta, normalized);
|
|
123
|
+
}
|
|
124
|
+
|
|
73
125
|
await this.#db.upsert_known_entry.run({
|
|
74
126
|
run_id: runId,
|
|
75
127
|
loop_id: loopId,
|
|
76
128
|
turn,
|
|
77
|
-
path:
|
|
129
|
+
path: normalized,
|
|
78
130
|
body,
|
|
79
131
|
status,
|
|
80
132
|
fidelity,
|
|
@@ -82,14 +134,19 @@ export default class KnownStore {
|
|
|
82
134
|
attributes: attributes ? JSON.stringify(attributes) : null,
|
|
83
135
|
updated_at: updatedAt,
|
|
84
136
|
});
|
|
137
|
+
this.#emitChanged(runId, normalized, "upsert");
|
|
138
|
+
|
|
139
|
+
if (delta > 0) this.#budgetGuard?.charge(delta);
|
|
85
140
|
}
|
|
86
141
|
|
|
87
142
|
async promote(runId, path, turn) {
|
|
143
|
+
const normalized = KnownStore.normalizePath(path);
|
|
88
144
|
await this.#db.promote_path.run({
|
|
89
145
|
run_id: runId,
|
|
90
|
-
path:
|
|
146
|
+
path: normalized,
|
|
91
147
|
turn,
|
|
92
148
|
});
|
|
149
|
+
this.#emitChanged(runId, normalized, "promote");
|
|
93
150
|
}
|
|
94
151
|
|
|
95
152
|
async setFileFidelity(runId, pattern, fidelity) {
|
|
@@ -101,28 +158,35 @@ export default class KnownStore {
|
|
|
101
158
|
if (result.changes === 0) {
|
|
102
159
|
await this.upsert(runId, 0, pattern, "", 200, { fidelity });
|
|
103
160
|
}
|
|
161
|
+
this.#emitChanged(runId, pattern, "fidelity");
|
|
104
162
|
}
|
|
105
163
|
|
|
106
164
|
async setFidelity(runId, path, fidelity) {
|
|
165
|
+
const normalized = KnownStore.normalizePath(path);
|
|
107
166
|
await this.#db.set_fidelity.run({
|
|
108
167
|
run_id: runId,
|
|
109
|
-
path:
|
|
168
|
+
path: normalized,
|
|
110
169
|
fidelity,
|
|
111
170
|
});
|
|
171
|
+
this.#emitChanged(runId, normalized, "fidelity");
|
|
112
172
|
}
|
|
113
173
|
|
|
114
174
|
async demote(runId, path) {
|
|
175
|
+
const normalized = KnownStore.normalizePath(path);
|
|
115
176
|
await this.#db.demote_path.run({
|
|
116
177
|
run_id: runId,
|
|
117
|
-
path:
|
|
178
|
+
path: normalized,
|
|
118
179
|
});
|
|
180
|
+
this.#emitChanged(runId, normalized, "demote");
|
|
119
181
|
}
|
|
120
182
|
|
|
121
183
|
async remove(runId, path) {
|
|
184
|
+
const normalized = KnownStore.normalizePath(path);
|
|
122
185
|
await this.#db.delete_known_entry.run({
|
|
123
186
|
run_id: runId,
|
|
124
|
-
path:
|
|
187
|
+
path: normalized,
|
|
125
188
|
});
|
|
189
|
+
this.#emitChanged(runId, normalized, "remove");
|
|
126
190
|
}
|
|
127
191
|
|
|
128
192
|
async removeFilesByPattern(runId, pattern) {
|
|
@@ -130,6 +194,7 @@ export default class KnownStore {
|
|
|
130
194
|
run_id: runId,
|
|
131
195
|
pattern,
|
|
132
196
|
});
|
|
197
|
+
this.#emitChanged(runId, pattern, "remove");
|
|
133
198
|
}
|
|
134
199
|
|
|
135
200
|
static #bodyPattern(body) {
|
|
@@ -137,12 +202,30 @@ export default class KnownStore {
|
|
|
137
202
|
}
|
|
138
203
|
|
|
139
204
|
async promoteByPattern(runId, path, body, turn) {
|
|
205
|
+
let cost = 0;
|
|
206
|
+
if (this.#budgetGuard) {
|
|
207
|
+
const entries = await this.#db.get_entries_by_pattern.all({
|
|
208
|
+
run_id: runId,
|
|
209
|
+
path,
|
|
210
|
+
body: KnownStore.#bodyPattern(body),
|
|
211
|
+
limit: null,
|
|
212
|
+
offset: null,
|
|
213
|
+
});
|
|
214
|
+
cost = entries
|
|
215
|
+
.filter((e) => e.fidelity === "archive" || e.fidelity === "index")
|
|
216
|
+
.reduce((sum, e) => sum + (e.tokens_full || 0), 0);
|
|
217
|
+
if (cost > 0) this.#budgetGuard.check(cost, path);
|
|
218
|
+
}
|
|
219
|
+
|
|
140
220
|
await this.#db.promote_by_pattern.run({
|
|
141
221
|
run_id: runId,
|
|
142
222
|
path,
|
|
143
223
|
body: KnownStore.#bodyPattern(body),
|
|
144
224
|
turn,
|
|
145
225
|
});
|
|
226
|
+
this.#emitChanged(runId, path, "promote");
|
|
227
|
+
|
|
228
|
+
if (cost > 0) this.#budgetGuard?.charge(cost);
|
|
146
229
|
}
|
|
147
230
|
|
|
148
231
|
async demoteByPattern(runId, path, body) {
|
|
@@ -151,6 +234,7 @@ export default class KnownStore {
|
|
|
151
234
|
path,
|
|
152
235
|
body: KnownStore.#bodyPattern(body),
|
|
153
236
|
});
|
|
237
|
+
this.#emitChanged(runId, path, "demote");
|
|
154
238
|
}
|
|
155
239
|
|
|
156
240
|
async getEntriesByPattern(runId, path, body, { limit, offset } = {}) {
|
|
@@ -169,30 +253,58 @@ export default class KnownStore {
|
|
|
169
253
|
path,
|
|
170
254
|
body: KnownStore.#bodyPattern(body),
|
|
171
255
|
});
|
|
256
|
+
this.#emitChanged(runId, path, "remove");
|
|
172
257
|
}
|
|
173
258
|
|
|
174
259
|
async updateBodyByPattern(runId, path, body, newBody) {
|
|
260
|
+
let delta = 0;
|
|
261
|
+
if (this.#budgetGuard) {
|
|
262
|
+
const entries = await this.#db.get_entries_by_pattern.all({
|
|
263
|
+
run_id: runId,
|
|
264
|
+
path,
|
|
265
|
+
body: KnownStore.#bodyPattern(body),
|
|
266
|
+
limit: null,
|
|
267
|
+
offset: null,
|
|
268
|
+
});
|
|
269
|
+
const visible = entries.filter((e) =>
|
|
270
|
+
this.#isVisible(e.path, e.fidelity),
|
|
271
|
+
);
|
|
272
|
+
const oldTotal = visible.reduce((sum, e) => sum + (e.tokens || 0), 0);
|
|
273
|
+
const newTokensPer = countTokens(newBody);
|
|
274
|
+
delta = newTokensPer * visible.length - oldTotal;
|
|
275
|
+
if (delta > 0) this.#budgetGuard.check(delta, path);
|
|
276
|
+
}
|
|
277
|
+
|
|
175
278
|
await this.#db.update_body_by_pattern.run({
|
|
176
279
|
run_id: runId,
|
|
177
280
|
path,
|
|
178
281
|
body: KnownStore.#bodyPattern(body),
|
|
179
282
|
new_body: newBody,
|
|
180
283
|
});
|
|
284
|
+
this.#emitChanged(runId, path, "body");
|
|
285
|
+
|
|
286
|
+
if (delta > 0) this.#budgetGuard?.charge(delta);
|
|
181
287
|
}
|
|
182
288
|
|
|
183
289
|
async resolve(runId, path, status, body) {
|
|
290
|
+
const normalized = KnownStore.normalizePath(path);
|
|
184
291
|
await this.#db.resolve_known_entry.run({
|
|
185
292
|
run_id: runId,
|
|
186
|
-
path:
|
|
293
|
+
path: normalized,
|
|
187
294
|
status,
|
|
188
295
|
body,
|
|
189
296
|
});
|
|
297
|
+
this.#emitChanged(runId, normalized, "resolve");
|
|
190
298
|
}
|
|
191
299
|
|
|
192
300
|
async getLog(runId) {
|
|
193
301
|
return this.#db.get_results.all({ run_id: runId });
|
|
194
302
|
}
|
|
195
303
|
|
|
304
|
+
async getEntries(runId) {
|
|
305
|
+
return this.#db.get_known_entries.all({ run_id: runId });
|
|
306
|
+
}
|
|
307
|
+
|
|
196
308
|
async getFileEntries(runId) {
|
|
197
309
|
return this.#db.get_file_entries.all({ run_id: runId });
|
|
198
310
|
}
|
|
@@ -237,11 +349,13 @@ export default class KnownStore {
|
|
|
237
349
|
}
|
|
238
350
|
|
|
239
351
|
async setAttributes(runId, path, attrs) {
|
|
352
|
+
const normalized = KnownStore.normalizePath(path);
|
|
240
353
|
await this.#db.update_entry_attributes.run({
|
|
241
354
|
run_id: runId,
|
|
242
|
-
path:
|
|
355
|
+
path: normalized,
|
|
243
356
|
attributes: JSON.stringify(attrs),
|
|
244
357
|
});
|
|
358
|
+
this.#emitChanged(runId, normalized, "attributes");
|
|
245
359
|
}
|
|
246
360
|
|
|
247
361
|
async getState(runId, path) {
|
|
@@ -14,7 +14,10 @@ export default class ProjectAgent {
|
|
|
14
14
|
this.#db = db;
|
|
15
15
|
this.#hooks = hooks;
|
|
16
16
|
this.#llm = new LlmProvider(db);
|
|
17
|
-
this.#knownStore = new KnownStore(db
|
|
17
|
+
this.#knownStore = new KnownStore(db, {
|
|
18
|
+
onChanged: (event) => hooks.entry.changed.emit(event).catch(() => {}),
|
|
19
|
+
});
|
|
20
|
+
this.#knownStore.loadSchemes(db);
|
|
18
21
|
|
|
19
22
|
const turnExecutor = new TurnExecutor(
|
|
20
23
|
db,
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
const MAX_STALLS = Number(process.env.RUMMY_MAX_STALLS) || 3;
|
|
2
2
|
const MAX_REPETITIONS = Number(process.env.RUMMY_MAX_REPETITIONS) || 3;
|
|
3
|
+
const MAX_UPDATE_REPEATS = Number(process.env.RUMMY_MAX_UPDATE_REPEATS) || 3;
|
|
3
4
|
|
|
4
5
|
export default class ResponseHealer {
|
|
5
6
|
#stallCount = 0;
|
|
6
7
|
#lastFingerprint = null;
|
|
7
8
|
#repetitionCount = 0;
|
|
9
|
+
#lastUpdateText = null;
|
|
10
|
+
#updateRepeatCount = 0;
|
|
8
11
|
|
|
9
12
|
/**
|
|
10
13
|
* Heal a missing status tag. Called when the model emits
|
|
@@ -97,7 +100,7 @@ export default class ResponseHealer {
|
|
|
97
100
|
* neither present → warn, increment stall counter, continue
|
|
98
101
|
* stall counter hits MAX_STALLS → force-complete
|
|
99
102
|
*/
|
|
100
|
-
assessProgress({ summaryText, updateText, statusHealed }) {
|
|
103
|
+
assessProgress({ summaryText, updateText, statusHealed, flags }) {
|
|
101
104
|
if (summaryText) {
|
|
102
105
|
this.#stallCount = 0;
|
|
103
106
|
return { continue: false };
|
|
@@ -105,6 +108,21 @@ export default class ResponseHealer {
|
|
|
105
108
|
|
|
106
109
|
if (updateText && !statusHealed) {
|
|
107
110
|
this.#stallCount = 0;
|
|
111
|
+
// Track repeated update text — model stuck declaring readiness
|
|
112
|
+
// But if the model created new entries this turn, it's making
|
|
113
|
+
// progress even if the update text is the same.
|
|
114
|
+
const madeProgress = flags?.hasWrites || flags?.hasReads;
|
|
115
|
+
if (updateText === this.#lastUpdateText && !madeProgress) {
|
|
116
|
+
this.#updateRepeatCount++;
|
|
117
|
+
if (this.#updateRepeatCount >= MAX_UPDATE_REPEATS) {
|
|
118
|
+
const reason = `Same <update/> repeated ${this.#updateRepeatCount} turns: "${updateText.slice(0, 60)}"`;
|
|
119
|
+
console.warn(`[RUMMY] Stalled: ${reason}. Force-completing.`);
|
|
120
|
+
return { continue: false, reason };
|
|
121
|
+
}
|
|
122
|
+
} else {
|
|
123
|
+
this.#lastUpdateText = updateText;
|
|
124
|
+
this.#updateRepeatCount = 1;
|
|
125
|
+
}
|
|
108
126
|
return { continue: true };
|
|
109
127
|
}
|
|
110
128
|
|
|
@@ -130,5 +148,7 @@ export default class ResponseHealer {
|
|
|
130
148
|
this.#stallCount = 0;
|
|
131
149
|
this.#lastFingerprint = null;
|
|
132
150
|
this.#repetitionCount = 0;
|
|
151
|
+
this.#lastUpdateText = null;
|
|
152
|
+
this.#updateRepeatCount = 0;
|
|
133
153
|
}
|
|
134
154
|
}
|