@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
package/src/agent/AgentLoop.js
CHANGED
|
@@ -121,6 +121,7 @@ export default class AgentLoop {
|
|
|
121
121
|
const noRepo = options?.noRepo === true;
|
|
122
122
|
const noInteraction = options?.noInteraction === true;
|
|
123
123
|
const noWeb = options?.noWeb === true;
|
|
124
|
+
const noProposals = options?.noProposals === true;
|
|
124
125
|
const requestedModel = model;
|
|
125
126
|
|
|
126
127
|
const runInfo = await this.#ensureRun(projectId, model, run, options);
|
|
@@ -146,6 +147,7 @@ export default class AgentLoop {
|
|
|
146
147
|
noRepo,
|
|
147
148
|
noInteraction,
|
|
148
149
|
noWeb,
|
|
150
|
+
noProposals,
|
|
149
151
|
temperature: options?.temperature,
|
|
150
152
|
}),
|
|
151
153
|
});
|
|
@@ -164,100 +166,76 @@ export default class AgentLoop {
|
|
|
164
166
|
}
|
|
165
167
|
|
|
166
168
|
async #drainQueue(currentRunId, currentAlias, projectId, project, options) {
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
while (true) {
|
|
170
|
-
const loop = await this.#db.claim_next_loop.get({
|
|
171
|
-
run_id: currentRunId,
|
|
172
|
-
});
|
|
173
|
-
if (!loop) break;
|
|
174
|
-
|
|
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
|
-
|
|
183
|
-
const result = await this.#executeLoop({
|
|
184
|
-
mode: loop.mode,
|
|
185
|
-
project,
|
|
186
|
-
projectId,
|
|
187
|
-
currentRunId,
|
|
188
|
-
currentAlias,
|
|
189
|
-
currentLoopId: loop.id,
|
|
190
|
-
requestedModel: loop.model,
|
|
191
|
-
prompt: loop.prompt,
|
|
192
|
-
noRepo: loopConfig.noRepo || false,
|
|
193
|
-
noInteraction: loopConfig.noInteraction || false,
|
|
194
|
-
noWeb: loopConfig.noWeb || false,
|
|
195
|
-
options: { ...options, temperature: loopConfig.temperature },
|
|
196
|
-
hook,
|
|
197
|
-
});
|
|
169
|
+
const controller = new AbortController();
|
|
170
|
+
this.#activeRuns.set(currentRunId, controller);
|
|
198
171
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
result: JSON.stringify(result),
|
|
172
|
+
try {
|
|
173
|
+
while (true) {
|
|
174
|
+
const loop = await this.#db.claim_next_loop.get({
|
|
175
|
+
run_id: currentRunId,
|
|
204
176
|
});
|
|
177
|
+
if (!loop) break;
|
|
178
|
+
|
|
179
|
+
const loopConfig = loop.config ? JSON.parse(loop.config) : {};
|
|
180
|
+
const hook = loop.mode === "ask" ? this.#hooks.ask : this.#hooks.act;
|
|
181
|
+
|
|
182
|
+
let result;
|
|
183
|
+
try {
|
|
184
|
+
result = await this.#executeLoop({
|
|
185
|
+
mode: loop.mode,
|
|
186
|
+
project,
|
|
187
|
+
projectId,
|
|
188
|
+
currentRunId,
|
|
189
|
+
currentAlias,
|
|
190
|
+
currentLoopId: loop.id,
|
|
191
|
+
requestedModel: loop.model,
|
|
192
|
+
prompt: loop.prompt,
|
|
193
|
+
noRepo: loopConfig.noRepo || false,
|
|
194
|
+
noInteraction: loopConfig.noInteraction || false,
|
|
195
|
+
noWeb: loopConfig.noWeb || false,
|
|
196
|
+
noProposals: loopConfig.noProposals || false,
|
|
197
|
+
options: { ...options, temperature: loopConfig.temperature },
|
|
198
|
+
hook,
|
|
199
|
+
signal: controller.signal,
|
|
200
|
+
});
|
|
201
|
+
} catch (err) {
|
|
202
|
+
await this.#db.complete_loop.run({
|
|
203
|
+
id: loop.id,
|
|
204
|
+
status: 500,
|
|
205
|
+
result: JSON.stringify({ error: err.message }),
|
|
206
|
+
});
|
|
207
|
+
throw err;
|
|
208
|
+
}
|
|
205
209
|
|
|
206
|
-
|
|
207
|
-
|
|
210
|
+
if (result.status === 413) {
|
|
211
|
+
await this.#db.complete_loop.run({
|
|
212
|
+
id: loop.id,
|
|
213
|
+
status: 413,
|
|
214
|
+
result: JSON.stringify(result),
|
|
215
|
+
});
|
|
208
216
|
return {
|
|
209
217
|
run: currentAlias,
|
|
210
218
|
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).`,
|
|
219
|
+
error: `Context full (${result.overflow} tokens over).`,
|
|
215
220
|
};
|
|
216
221
|
}
|
|
217
222
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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,
|
|
223
|
+
await this.#db.complete_loop.run({
|
|
224
|
+
id: loop.id,
|
|
225
|
+
status: result.status === 202 ? 202 : result.status,
|
|
226
|
+
result: JSON.stringify(result),
|
|
245
227
|
});
|
|
246
228
|
|
|
247
|
-
|
|
229
|
+
if (result.status === 202) return result;
|
|
248
230
|
}
|
|
249
231
|
|
|
250
|
-
await this.#db.
|
|
251
|
-
|
|
252
|
-
status: result.status === 202 ? 202 : result.status,
|
|
253
|
-
result: JSON.stringify(result),
|
|
232
|
+
const runRow = await this.#db.get_run_by_alias.get({
|
|
233
|
+
alias: currentAlias,
|
|
254
234
|
});
|
|
255
|
-
|
|
256
|
-
|
|
235
|
+
return { run: currentAlias, status: runRow?.status ?? 200 };
|
|
236
|
+
} finally {
|
|
237
|
+
this.#activeRuns.delete(currentRunId);
|
|
257
238
|
}
|
|
258
|
-
|
|
259
|
-
const runRow = await this.#db.get_run_by_alias.get({ alias: currentAlias });
|
|
260
|
-
return { run: currentAlias, status: runRow?.status ?? 200 };
|
|
261
239
|
}
|
|
262
240
|
|
|
263
241
|
async #executeLoop({
|
|
@@ -272,8 +250,10 @@ export default class AgentLoop {
|
|
|
272
250
|
noRepo,
|
|
273
251
|
noInteraction,
|
|
274
252
|
noWeb,
|
|
253
|
+
noProposals,
|
|
275
254
|
options,
|
|
276
255
|
hook,
|
|
256
|
+
signal,
|
|
277
257
|
}) {
|
|
278
258
|
const runRow = await this.#db.get_run_by_id.get({ id: currentRunId });
|
|
279
259
|
if (runRow.status !== 102) {
|
|
@@ -292,18 +272,27 @@ export default class AgentLoop {
|
|
|
292
272
|
const toolSet = this.#hooks.tools.resolveForLoop(mode, {
|
|
293
273
|
noInteraction,
|
|
294
274
|
noWeb,
|
|
275
|
+
noProposals,
|
|
295
276
|
});
|
|
296
277
|
|
|
297
278
|
let loopIteration = 0;
|
|
298
279
|
const MAX_LOOP_ITERATIONS = Number(process.env.RUMMY_MAX_TURNS) || 15;
|
|
299
280
|
const healer = new ResponseHealer();
|
|
300
281
|
|
|
301
|
-
const controller = new AbortController();
|
|
302
|
-
this.#activeRuns.set(currentRunId, controller);
|
|
303
|
-
|
|
304
282
|
let _lastAssembledTokens = 0;
|
|
305
|
-
let
|
|
306
|
-
|
|
283
|
+
let recovery = null; // { target, promptPath, strikes, lastTokens }
|
|
284
|
+
|
|
285
|
+
// Demote full logging entries from previous loops to summary before
|
|
286
|
+
// they appear in <previous>. General policy: keep <previous> compact.
|
|
287
|
+
await this.#knownStore.demotePreviousLoopLogging(
|
|
288
|
+
currentRunId,
|
|
289
|
+
currentLoopId,
|
|
290
|
+
);
|
|
291
|
+
|
|
292
|
+
// Restore any prompt entries left at summary fidelity by a recovery
|
|
293
|
+
// phase that was interrupted (server crash, restart). If the full
|
|
294
|
+
// prompt would overflow, Prompt Demotion on turn 1 handles it.
|
|
295
|
+
await this.#knownStore.restoreSummarizedPrompts(currentRunId);
|
|
307
296
|
|
|
308
297
|
await this.#hooks.loop.started.emit({
|
|
309
298
|
runId: currentRunId,
|
|
@@ -314,7 +303,7 @@ export default class AgentLoop {
|
|
|
314
303
|
|
|
315
304
|
try {
|
|
316
305
|
while (loopIteration < MAX_LOOP_ITERATIONS) {
|
|
317
|
-
if (
|
|
306
|
+
if (signal.aborted) {
|
|
318
307
|
await this.#db.update_run_status.run({
|
|
319
308
|
id: currentRunId,
|
|
320
309
|
status: 499,
|
|
@@ -332,8 +321,6 @@ export default class AgentLoop {
|
|
|
332
321
|
let turnPrompt;
|
|
333
322
|
if (loopIteration === 1) {
|
|
334
323
|
turnPrompt = prompt;
|
|
335
|
-
} else if (mode === "panic") {
|
|
336
|
-
turnPrompt = "Continue freeing space. Check <knowns> token counts.";
|
|
337
324
|
} else {
|
|
338
325
|
turnPrompt = this.#buildContinuationPrompt(
|
|
339
326
|
loopIteration,
|
|
@@ -350,14 +337,15 @@ export default class AgentLoop {
|
|
|
350
337
|
currentLoopId,
|
|
351
338
|
requestedModel,
|
|
352
339
|
loopPrompt: turnPrompt,
|
|
340
|
+
loopIteration,
|
|
353
341
|
noRepo,
|
|
354
342
|
toolSet,
|
|
343
|
+
inRecovery: recovery !== null,
|
|
355
344
|
contextSize,
|
|
356
345
|
options: { ...options, isContinuation: loopIteration > 1 },
|
|
357
|
-
signal
|
|
346
|
+
signal,
|
|
358
347
|
});
|
|
359
348
|
|
|
360
|
-
// Budget overflow — return 413 to drainQueue for panic mode
|
|
361
349
|
if (result.status === 413) {
|
|
362
350
|
return {
|
|
363
351
|
run: currentAlias,
|
|
@@ -371,44 +359,28 @@ export default class AgentLoop {
|
|
|
371
359
|
|
|
372
360
|
_lastAssembledTokens = result.assembledTokens;
|
|
373
361
|
|
|
374
|
-
//
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
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;
|
|
362
|
+
// Budget recovery: enforce progress toward context target.
|
|
363
|
+
const ra = advanceRecovery(recovery, result);
|
|
364
|
+
recovery = ra.next;
|
|
365
|
+
if (ra.action === "restore" && ra.promptPath) {
|
|
366
|
+
await this.#knownStore.setFidelity(
|
|
367
|
+
currentRunId,
|
|
368
|
+
ra.promptPath,
|
|
369
|
+
"full",
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
if (ra.action === "hard413") {
|
|
373
|
+
await this.#db.update_run_status.run({
|
|
374
|
+
id: currentRunId,
|
|
375
|
+
status: 413,
|
|
376
|
+
});
|
|
377
|
+
const out = {
|
|
378
|
+
run: currentAlias,
|
|
379
|
+
status: 413,
|
|
380
|
+
turn: result.turn,
|
|
381
|
+
};
|
|
382
|
+
await hook.completed.emit({ projectId, ...out });
|
|
383
|
+
return out;
|
|
412
384
|
}
|
|
413
385
|
|
|
414
386
|
const runUsage = await this.#db.get_run_usage.get({
|
|
@@ -442,12 +414,13 @@ export default class AgentLoop {
|
|
|
442
414
|
model: result.model,
|
|
443
415
|
temperature: result.temperature,
|
|
444
416
|
context_size: result.contextSize,
|
|
445
|
-
context_tokens:
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
417
|
+
context_tokens:
|
|
418
|
+
(
|
|
419
|
+
await this.#db.get_turn_context_tokens.get({
|
|
420
|
+
run_id: currentRunId,
|
|
421
|
+
sequence: result.turn,
|
|
422
|
+
})
|
|
423
|
+
)?.context_tokens ?? 0,
|
|
451
424
|
prompt_tokens: runUsage.prompt_tokens,
|
|
452
425
|
cached_tokens: runUsage.cached_tokens,
|
|
453
426
|
completion_tokens: runUsage.completion_tokens,
|
|
@@ -482,6 +455,9 @@ export default class AgentLoop {
|
|
|
482
455
|
flags: result.flags,
|
|
483
456
|
});
|
|
484
457
|
|
|
458
|
+
// Don't exit while budget recovery is still active.
|
|
459
|
+
if (recovery !== null) continue;
|
|
460
|
+
|
|
485
461
|
const repetition = healer.assessRepetition(result);
|
|
486
462
|
if (!repetition.continue) {
|
|
487
463
|
await this.#db.update_run_status.run({
|
|
@@ -526,7 +502,7 @@ export default class AgentLoop {
|
|
|
526
502
|
await hook.completed.emit({ projectId, ...out });
|
|
527
503
|
return out;
|
|
528
504
|
} catch (err) {
|
|
529
|
-
if (
|
|
505
|
+
if (signal.aborted) {
|
|
530
506
|
await this.#db.update_run_status.run({
|
|
531
507
|
id: currentRunId,
|
|
532
508
|
status: 499,
|
|
@@ -558,7 +534,6 @@ export default class AgentLoop {
|
|
|
558
534
|
await hook.completed.emit({ projectId, ...out });
|
|
559
535
|
return out;
|
|
560
536
|
} finally {
|
|
561
|
-
this.#activeRuns.delete(currentRunId);
|
|
562
537
|
await this.#hooks.loop.completed
|
|
563
538
|
.emit({
|
|
564
539
|
runId: currentRunId,
|
|
@@ -599,6 +574,29 @@ export default class AgentLoop {
|
|
|
599
574
|
}
|
|
600
575
|
|
|
601
576
|
if (action === "accept") {
|
|
577
|
+
if (path.startsWith("set://") && attrs?.file && attrs?.merge) {
|
|
578
|
+
const fileBody = await this.#knownStore.getBody(runId, attrs.file);
|
|
579
|
+
if (fileBody != null) {
|
|
580
|
+
const blocks = attrs.merge.split(/(?=<<<<<<< SEARCH)/);
|
|
581
|
+
let patched = fileBody;
|
|
582
|
+
for (const block of blocks) {
|
|
583
|
+
const m = block.match(
|
|
584
|
+
/<<<<<<< SEARCH\n?([\s\S]*?)\n?=======\n?([\s\S]*?)\n?>>>>>>> REPLACE/,
|
|
585
|
+
);
|
|
586
|
+
if (m) patched = patched.replace(m[1], m[2]);
|
|
587
|
+
}
|
|
588
|
+
const turn = (await this.#db.get_run_by_id.get({ id: runId }))
|
|
589
|
+
.next_turn;
|
|
590
|
+
await this.#knownStore.upsert(
|
|
591
|
+
runId,
|
|
592
|
+
turn,
|
|
593
|
+
attrs.file,
|
|
594
|
+
patched,
|
|
595
|
+
200,
|
|
596
|
+
);
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
602
600
|
if (path.startsWith("rm://")) {
|
|
603
601
|
if (attrs?.path) {
|
|
604
602
|
await this.#knownStore.remove(runId, attrs.path);
|
|
@@ -676,7 +674,7 @@ export default class AgentLoop {
|
|
|
676
674
|
mode: resumeMode,
|
|
677
675
|
model: runRow.model,
|
|
678
676
|
prompt: "",
|
|
679
|
-
config: "{}",
|
|
677
|
+
config: currentLoop?.config || "{}",
|
|
680
678
|
});
|
|
681
679
|
return this.#drainQueue(runId, runAlias, projectId, project, {});
|
|
682
680
|
}
|
|
@@ -735,3 +733,51 @@ export default class AgentLoop {
|
|
|
735
733
|
return this.#knownStore.getLog(runRow.id);
|
|
736
734
|
}
|
|
737
735
|
}
|
|
736
|
+
|
|
737
|
+
/**
|
|
738
|
+
* Pure recovery state transition — exported for testing.
|
|
739
|
+
*
|
|
740
|
+
* @param {object|null} recovery Current recovery state (mutated copy returned).
|
|
741
|
+
* @param {{ assembledTokens: number, budgetRecovery?: { target: number, promptPath: string|null } }} result
|
|
742
|
+
* @returns {{ next: object|null, action: null|'restore'|'hard413', promptPath: string|null }}
|
|
743
|
+
*/
|
|
744
|
+
export function advanceRecovery(recovery, result) {
|
|
745
|
+
// Initialise or update recovery state from a new Turn Demotion event.
|
|
746
|
+
if (result.budgetRecovery) {
|
|
747
|
+
if (!recovery) {
|
|
748
|
+
recovery = {
|
|
749
|
+
target: result.budgetRecovery.target,
|
|
750
|
+
promptPath: result.budgetRecovery.promptPath,
|
|
751
|
+
strikes: 0,
|
|
752
|
+
lastTokens: result.assembledTokens,
|
|
753
|
+
};
|
|
754
|
+
} else {
|
|
755
|
+
// Re-overflow during recovery: tighten target, don't count as strike.
|
|
756
|
+
recovery = {
|
|
757
|
+
...recovery,
|
|
758
|
+
target: Math.min(recovery.target, result.budgetRecovery.target),
|
|
759
|
+
};
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
if (recovery === null) return { next: null, action: null, promptPath: null };
|
|
764
|
+
|
|
765
|
+
const current = result.assembledTokens;
|
|
766
|
+
|
|
767
|
+
if (current <= recovery.target) {
|
|
768
|
+
return { next: null, action: "restore", promptPath: recovery.promptPath };
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
const noProgress = current >= recovery.lastTokens && !result.budgetRecovery;
|
|
772
|
+
const strikes = noProgress ? recovery.strikes + 1 : 0;
|
|
773
|
+
|
|
774
|
+
if (strikes >= 3) {
|
|
775
|
+
return { next: null, action: "hard413", promptPath: null };
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
return {
|
|
779
|
+
next: { ...recovery, strikes, lastTokens: current },
|
|
780
|
+
action: null,
|
|
781
|
+
promptPath: null,
|
|
782
|
+
};
|
|
783
|
+
}
|
|
@@ -13,6 +13,7 @@ export default class ContextAssembler {
|
|
|
13
13
|
demoted = [],
|
|
14
14
|
toolSet = null,
|
|
15
15
|
lastContextTokens = 0,
|
|
16
|
+
turn = 1,
|
|
16
17
|
} = {},
|
|
17
18
|
hooks,
|
|
18
19
|
) {
|
|
@@ -30,6 +31,7 @@ export default class ContextAssembler {
|
|
|
30
31
|
lastContextTokens,
|
|
31
32
|
demoted,
|
|
32
33
|
toolSet,
|
|
34
|
+
turn,
|
|
33
35
|
};
|
|
34
36
|
|
|
35
37
|
const system = await hooks.assembly.system.filter(systemPrompt, ctx);
|
package/src/agent/KnownStore.js
CHANGED
|
@@ -1,25 +1,16 @@
|
|
|
1
1
|
import slugify from "../sql/functions/slugify.js";
|
|
2
|
-
import { countTokens } from "./tokens.js";
|
|
3
2
|
|
|
4
3
|
export default class KnownStore {
|
|
5
4
|
#db;
|
|
6
5
|
#onChanged;
|
|
7
|
-
#budgetGuard = null;
|
|
8
6
|
#schemes = new Map();
|
|
7
|
+
#seq = 0;
|
|
9
8
|
|
|
10
9
|
constructor(db, { onChanged } = {}) {
|
|
11
10
|
this.#db = db;
|
|
12
11
|
this.#onChanged = onChanged || null;
|
|
13
12
|
}
|
|
14
13
|
|
|
15
|
-
get budgetGuard() {
|
|
16
|
-
return this.#budgetGuard;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
set budgetGuard(guard) {
|
|
20
|
-
this.#budgetGuard = guard;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
14
|
async loadSchemes(db) {
|
|
24
15
|
const rows = await (db || this.#db).get_all_schemes.all();
|
|
25
16
|
this.#schemes.clear();
|
|
@@ -28,13 +19,6 @@ export default class KnownStore {
|
|
|
28
19
|
}
|
|
29
20
|
}
|
|
30
21
|
|
|
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
22
|
#emitChanged(runId, path, changeType) {
|
|
39
23
|
if (this.#onChanged) this.#onChanged({ runId, path, changeType });
|
|
40
24
|
}
|
|
@@ -46,15 +30,16 @@ export default class KnownStore {
|
|
|
46
30
|
|
|
47
31
|
static normalizePath(path) {
|
|
48
32
|
if (!path?.includes("://")) return path;
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
33
|
+
const sep = path.indexOf("://");
|
|
34
|
+
const scheme = path.slice(0, sep).toLowerCase();
|
|
35
|
+
const rest = path.slice(sep + 3);
|
|
36
|
+
try {
|
|
37
|
+
// Decode first (idempotent), then encode — but preserve slashes
|
|
38
|
+
const decoded = decodeURIComponent(rest);
|
|
39
|
+
return `${scheme}://${decoded.split("/").map(encodeURIComponent).join("/")}`;
|
|
40
|
+
} catch {
|
|
41
|
+
return `${scheme}://${rest.split("/").map(encodeURIComponent).join("/")}`;
|
|
42
|
+
}
|
|
58
43
|
}
|
|
59
44
|
|
|
60
45
|
async nextTurn(runId) {
|
|
@@ -71,15 +56,15 @@ export default class KnownStore {
|
|
|
71
56
|
path: candidate,
|
|
72
57
|
});
|
|
73
58
|
if (!existing) return candidate;
|
|
74
|
-
return `${candidate}_${
|
|
59
|
+
return `${candidate}_${++this.#seq}`;
|
|
75
60
|
}
|
|
76
61
|
|
|
77
62
|
async slugPath(runId, scheme, content, summary) {
|
|
78
|
-
const source = summary
|
|
63
|
+
const source = summary || content || "";
|
|
79
64
|
const base = slugify(source);
|
|
80
65
|
const prefix = `${scheme}://`;
|
|
81
66
|
|
|
82
|
-
if (!base) return `${prefix}${
|
|
67
|
+
if (!base) return `${prefix}${++this.#seq}`;
|
|
83
68
|
|
|
84
69
|
const candidate = `${prefix}${base}`;
|
|
85
70
|
const existing = await this.#db.get_entry_body.get({
|
|
@@ -88,7 +73,7 @@ export default class KnownStore {
|
|
|
88
73
|
});
|
|
89
74
|
if (!existing) return candidate;
|
|
90
75
|
|
|
91
|
-
return `${prefix}${base}_${
|
|
76
|
+
return `${prefix}${base}_${++this.#seq}`;
|
|
92
77
|
}
|
|
93
78
|
|
|
94
79
|
async upsert(
|
|
@@ -106,22 +91,6 @@ export default class KnownStore {
|
|
|
106
91
|
} = {},
|
|
107
92
|
) {
|
|
108
93
|
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
|
-
|
|
125
94
|
await this.#db.upsert_known_entry.run({
|
|
126
95
|
run_id: runId,
|
|
127
96
|
loop_id: loopId,
|
|
@@ -135,8 +104,6 @@ export default class KnownStore {
|
|
|
135
104
|
updated_at: updatedAt,
|
|
136
105
|
});
|
|
137
106
|
this.#emitChanged(runId, normalized, "upsert");
|
|
138
|
-
|
|
139
|
-
if (delta > 0) this.#budgetGuard?.charge(delta);
|
|
140
107
|
}
|
|
141
108
|
|
|
142
109
|
async promote(runId, path, turn) {
|
|
@@ -202,21 +169,6 @@ export default class KnownStore {
|
|
|
202
169
|
}
|
|
203
170
|
|
|
204
171
|
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
|
-
|
|
220
172
|
await this.#db.promote_by_pattern.run({
|
|
221
173
|
run_id: runId,
|
|
222
174
|
path,
|
|
@@ -224,8 +176,6 @@ export default class KnownStore {
|
|
|
224
176
|
turn,
|
|
225
177
|
});
|
|
226
178
|
this.#emitChanged(runId, path, "promote");
|
|
227
|
-
|
|
228
|
-
if (cost > 0) this.#budgetGuard?.charge(cost);
|
|
229
179
|
}
|
|
230
180
|
|
|
231
181
|
async demoteByPattern(runId, path, body) {
|
|
@@ -257,24 +207,6 @@ export default class KnownStore {
|
|
|
257
207
|
}
|
|
258
208
|
|
|
259
209
|
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
|
-
|
|
278
210
|
await this.#db.update_body_by_pattern.run({
|
|
279
211
|
run_id: runId,
|
|
280
212
|
path,
|
|
@@ -282,8 +214,6 @@ export default class KnownStore {
|
|
|
282
214
|
new_body: newBody,
|
|
283
215
|
});
|
|
284
216
|
this.#emitChanged(runId, path, "body");
|
|
285
|
-
|
|
286
|
-
if (delta > 0) this.#budgetGuard?.charge(delta);
|
|
287
217
|
}
|
|
288
218
|
|
|
289
219
|
async resolve(runId, path, status, body) {
|
|
@@ -297,6 +227,19 @@ export default class KnownStore {
|
|
|
297
227
|
this.#emitChanged(runId, normalized, "resolve");
|
|
298
228
|
}
|
|
299
229
|
|
|
230
|
+
async restoreSummarizedPrompts(runId) {
|
|
231
|
+
await this.#db.restore_summarized_prompts.run({ run_id: runId });
|
|
232
|
+
this.#emitChanged(runId, "prompt://batch", "fidelity");
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async demotePreviousLoopLogging(runId, loopId) {
|
|
236
|
+
await this.#db.demote_previous_loop_logging.run({
|
|
237
|
+
run_id: runId,
|
|
238
|
+
loop_id: loopId,
|
|
239
|
+
});
|
|
240
|
+
this.#emitChanged(runId, "logging://batch", "fidelity");
|
|
241
|
+
}
|
|
242
|
+
|
|
300
243
|
async getLog(runId) {
|
|
301
244
|
return this.#db.get_results.all({ run_id: runId });
|
|
302
245
|
}
|